<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/feeds/style.xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>niko-matsakis</title>
    <description>rssume processed feed for niko-matsakis</description>
    <link>/feeds/niko-matsakis</link>
    <atom:link href="/feeds/niko-matsakis" rel="self" type="application/rss+xml"/>
    <lastBuildDate>Thu, 4 Jun 2026 10:02:22 +0000</lastBuildDate>
    <generator>rssume</generator>
    <item>
      <title>您对 `dyn` 有什么想法？</title>
      <category>dyn-async-traits</category>
      <description>[AI 摘要] 本文探讨了 Rust 中 `dyn Trait` 的不足之处，并提出了改进其易用性、减少代码生成以及解决 Send/Sync 等问题的设想。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 本文探讨了 Rust 中 `dyn Trait` 的不足之处，并提出了改进其易用性、减少代码生成以及解决 Send/Sync 等问题的设想。</div><p>你好，我是dyn。您对 <code>dyn</code> 有什么想法？总的来说，我对 Rust 中 <code>dyn Trait</code> 的使用方式感到不满意，并且根据我与他人的交流，我相信我不是唯一这样想的人。然而，我也不完全确定最佳的修复方案。基于我上一篇文章，我想花一些时间探讨我对这个问题的理解。我很好奇其他人是否同意这里的观察结果，或者是否有其他补充。</p>
<h2 id="为什么我们有-dyn-trait">为什么我们有 <code>dyn Trait</code>？</h2>
<p>值得退后一步，问问为什么我们一开始就有 <code>dyn Trait</code>。在我看来，有两个充分的理由。</p>
<h3 id="因为有时您想谈论实现了-trait-的某个值">因为有时您想谈论“实现了 <code>Trait</code> 的某个值”</h3>
<p>最重要的是，这有时是严格必要的。例如，如果您正在构建一个像 <code>rayon</code> 或 <code>tokio</code> 这样的多线程运行时，您将需要在某处维护一个活动任务列表，每个任务都与用户代码中的某个闭包相关联。您不能用枚举来构建它，因为您无法在任何地方列举所有闭包。您需要类似 <code>Vec&lt;Box&lt;dyn ActiveTask&gt;&gt;</code> 的东西。</p>
<h3 id="因为有时您不需要那么多代码">因为有时您不需要那么多代码</h3>
<p>第二个原因是帮助缩短编译时间。Rust 社区非常依赖泛型类型和 <code>impl Trait</code>。这样做有充分的理由：它们允许编译器生成非常高效的代码。但另一方面，它们也迫使编译器生成大量（非常高效的）代码。明智地使用 <code>dyn Trait</code> 可以将一整组“几乎相同”的结构体和函数折叠成一个。</p>
<h3 id="这两个目标是不同的">这两个目标是不同的</h3>
<p>目前，这两个目标在 Rust 中都通过 <code>dyn Trait</code> 来表达，但实际上它们截然不同。对于第一个目标，您确实希望能够谈论拥有一个 <code>dyn Trait</code>。对于第二个目标，您可能更喜欢用泛型编写代码，但以一种不同的模式进行编译，在这种模式下，所涉及类型的具体信息被擦除，就像 Haskell 和 Swift 编译器的工作方式一样。</p>
<h2 id="当您真正想要一个-dyn-时更好的情况是什么样的">当您真正想要一个 <code>dyn</code> 时，“更好”的情况是什么样的？</h2>
<p>既然我们已经有了两个目标，让我们来谈谈我看到的围绕 <code>dyn Trait</code> 的一些具体问题，以及 <code>dyn Trait</code> 变得“更好”可能意味着什么。我们将从您真正<em>想要</em>一个 <code>dyn</code> 值的情况开始。</p>
<h3 id="观察您知道它是一个-dyn">观察：您知道它是一个 <code>dyn</code></h3>
<p>这种情况一个有趣的地方在于，根据定义，您正在显式地存储一个 <code>dyn Trait</code>。也就是说，您不是在处理一个 <code>T: ?Sized + Trait</code>，而其中的 <code>T</code> 恰好是 <code>dyn Trait</code>。这一点很重要，因为它开辟了设计空间。我们在上一篇博文中讨论过这一点：这意味着处理这个 <code>dyn Trait</code> 不必与处理任何其他实现 <code>Trait</code> 的 <code>T</code> 完全相同（在上一篇文章中，我们利用这一点指出，在 <code>dyn</code> trait 上调用异步函数必须在 <code>.box</code> 上下文中进行）。</p>
<h3 id="能够避免-box">能够避免 <code>Box</code></h3>
<p>对于这种模式，今天您几乎肯定会用 <code>Box&lt;dyn Task&gt;</code> 或者（较少见）<code>Arc&lt;dyn Task&gt;</code> 来表示您的任务。两者都是“宽指针”，由一个数据指针和一个虚函数表指针组成。数据指针会指向堆上的某个位置。</p>
<p>实际上，人们通常希望有一个“扁平化”的表示，它将虚函数表与固定数量的空间结合在一起，这些空间可能是一个指针，也可能不是。这对于实现类似 <code>Vec&lt;dyn Task&gt;</code> 的等效结构特别有用。今天实现这一点需要使用 unsafe 代码（<code>anyhow::Anyhow</code> 类型就是一个例子）。</p>
<h3 id="能够内联虚函数表">能够内联虚函数表</h3>
<p>另一种减小 <code>Box&lt;dyn Task&gt;</code> 大小的方法是将虚函数表“内联”存储在值的前部，这样 <code>Box&lt;dyn Task&gt;</code> 就是一个单一指针。这是 C++ 和 Java 编译器通常采用的方式，至少对于单继承是如此。我们在 Rust 中没有采用这种方法，因为 Rust 允许为外部类型实现本地 trait，因此不可能预先枚举一个类型的所有方法并将它们放入一个单一的虚函数表中。相反，我们为每个（类型，trait）对创建自定义虚函数表。</p>
<h3 id="能够处理-self-方法">能够处理 <code>self</code> 方法</h3>
<p>目前 <code>dyn</code> trait 不能有 <code>self</code> 方法。这意味着例如您不能有一个 <code>Box&lt;dyn FnOnce()&gt;</code> 闭包。您可以通过使用 <code>Box&lt;Self&gt;</code> 方法来解决这个问题，但这很烦人：</p>
<div><pre><code><span><span><span>trait</span><span> </span><span>Thunk</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>fn</span> <span>call</span><span>(</span><span>self</span>: <span>Box</span><span>&lt;</span><span>Self</span><span>&gt;</span><span>);</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>impl</span><span>&lt;</span><span>F</span><span>&gt;</span><span> </span><span>Thunk</span><span> </span><span>for</span><span> </span><span>F</span><span>
</span></span></span><span><span><span>where</span><span>
</span></span></span><span><span><span>    </span><span>F</span>: <span>FnOnce</span><span>(),</span><span>
</span></span></span><span><span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>fn</span> <span>call</span><span>(</span><span>self</span>: <span>Box</span><span>&lt;</span><span>Self</span><span>&gt;</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>(</span><span>*</span><span>self</span><span>)()</span><span>
</span></span></span><span><span><span>    </span><span>}</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>fn</span> <span>make_thunk</span><span>(</span><span>f</span>: <span>impl</span><span> </span><span>FnOnce</span><span>())</span><span> </span>-&gt; <span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>Thunk</span><span>&gt;</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>Box</span>::<span>new</span><span>(</span><span>f</span><span>)</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><h3 id="能够调用-clone">能够调用 <code>Clone</code></h3>
<p>有一个特定的问题经常困扰着我，那就是我希望能够克隆一个 <code>dyn</code> 值：</p>
<div><pre><code><span><span><span>trait</span><span> </span><span>Task</span>: <span>Clone</span> <span>{</span><span>
</span></span></span><span><span><span>    </span><span>//      ----- 错误：不是 dyn 兼容的
</span></span></span><span><span><span>    </span><span>fn</span> <span>method</span><span>(</span><span>&amp;</span><span>self</span><span>);</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>fn</span> <span>clone_task</span><span>(</span><span>task</span>: <span>&amp;</span><span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>Task</span><span>&gt;</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>task</span><span>.</span><span>clone</span><span>()</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>这很难修复，因为 <code>Clone</code> trait 只能为 <code>Sized</code> 类型实现。但是，天啊，如果能实现就好了。</p>
<h3 id="能够与至少某些泛型函数一起工作">能够与（至少某些）泛型函数一起工作</h3>
<p>在上述基础上，我希望有具有带泛型参数方法的 <code>dyn</code> trait。我不确定这能有多灵活，但能得到任何改善都是好的。我能想到的最简单的起点是允许在参数位置使用 <code>impl Trait</code>：</p>
<div><pre><code><span><span><span>trait</span><span> </span><span>Log</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>fn</span> <span>log_to</span><span>(</span><span>&amp;</span><span>self</span><span>,</span><span> </span><span>logger</span>: <span>impl</span><span> </span><span>Logger</span><span>);</span><span> </span><span>// &lt;-- 目前不是 dyn 兼容的
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>今天这个方法不是 dyn 兼容的，因为我们必须知道 <code>logger</code> 参数的类型来生成单态化的副本，所以我们不知道在虚函数表中放什么。可以想象，<em>如果</em> <code>Logger</code> trait 是 dyn 兼容的，我们可以生成一个副本，它（实际上）接受一个 <code>dyn Logger</code>——只不过这不太行得通，因为 <code>impl Logger</code> 是 <code>impl Logger + Sized</code> 的缩写，而 <code>dyn Logger</code> 不是 <code>Sized</code> 的。但也许我们可以巧妙处理。</p>
<p>如果我们支持在参数位置使用 <code>impl Logger</code>，那么最好也支持在返回位置使用它。这当然与我们为支持 dyn async trait 所要解决的问题大致相同：</p>
<div><pre><code><span><span><span>trait</span><span> </span><span>Signal</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>fn</span> <span>signal</span><span>(</span><span>&amp;</span><span>self</span><span>)</span><span> </span>-&gt; <span>impl</span><span> </span><span>Future</span><span>&lt;</span><span>Output</span><span> </span><span>=</span><span> </span><span>()</span><span>&gt;</span><span>;</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>除此之外，嗯，我不确定我们能把这延伸多远，但能够支持其他模式也会是<em>很好</em>的。</p>
<h3 id="能够与部分-trait-或未指定某些关联类型的-trait-一起工作">能够与部分 trait 或未指定某些关联类型的 trait 一起工作</h3>
<p>最后一点是，<em>有时</em>在这种情况下，我并不需要能够访问 trait 中的所有方法。有时我只通过 <code>dyn</code> 执行几个特定的操作。但目前，所有方法都必须是 dyn 兼容的，我才能用 <code>dyn</code> 使用它们。此外，我必须指定所有关联类型的值，以防它们出现在某个方法签名中。您可以通过将方法分解到一个超 trait 中来解决这个问题，但这假设该 trait 在您的控制之下，而且无论如何这很烦人。如果您能够拥有一个 trait 的部分视图就好了。</p>
<h2 id="当您真正想要更少代码时更好的情况是什么样的">当您真正想要更少代码时，“更好”的情况是什么样的？</h2>
<p>那么，如果泛型是可以的，甚至是好的，但您只是想避免生成那么多代码呢？您可能也希望这由您的用户控制。</p>
<p>我将通过一个代码示例来演示这一部分，展示您今天能做什么，以及会遇到什么样的问题。假设我正在编写一个自定义迭代器方法 <code>alternate</code>，它返回一个迭代器，该迭代器在原始迭代器的项和调用函数的结果之间交替。我可能会有这样的结构体：</p>
<div><pre><code><span><span><span>struct</span><span> </span><span>Alternate</span><span>&lt;</span><span>I</span>: <span>Iterator</span><span>,</span><span> </span><span>F</span>: <span>Fn</span><span>()</span><span> </span>-&gt; <span>I</span>::<span>Item</span><span>&gt;</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>base</span>: <span>I</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>func</span>: <span>F</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>call_func</span>: <span>bool</span><span>,</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>pub</span><span> </span><span>fn</span> <span>alternate</span><span>&lt;</span><span>I</span><span>,</span><span> </span><span>F</span><span>&gt;</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>base</span>: <span>I</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>func</span>: <span>F</span><span>,</span><span>
</span></span></span><span><span><span>)</span><span> </span>-&gt; <span>Alternate</span><span>&lt;</span><span>I</span><span>,</span><span> </span><span>F</span><span>&gt;</span><span>
</span></span></span><span><span><span>where</span><span>
</span></span></span><span><span><span>    </span><span>I</span>: <span>Iterator</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>F</span>: <span>Fn</span><span>()</span><span> </span>-&gt; <span>I</span>::<span>Item</span><span>,</span><span>
</span></span></span><span><span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>Alternate</span><span> </span><span>{</span><span> </span><span>base</span><span>,</span><span> </span><span>func</span><span>,</span><span> </span><span>call_func</span>: <span>false</span><span> </span><span>}</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p><code>Iterator</code> 的实现本身可能看起来像这样：</p>
<div><pre><code><span><span><span>impl</span><span>&lt;</span><span>I</span><span>,</span><span> </span><span>F</span><span>&gt;</span><span> </span><span>Iterator</span><span> </span><span>for</span><span> </span><span>Alternate</span><span>&lt;</span><span>I</span><span>,</span><span> </span><span>F</span><span>&gt;</span><span>
</span></span></span><span><span><span>where</span><span>
</span></span></span><span><span><span>    </span><span>I</span>: <span>Iterator</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>F</span>: <span>Fn</span><span>()</span><span> </span>-&gt; <span>I</span>::<span>Item</span><span>,</span><span>
</span></span></span><span><span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>type</span> <span>Item</span><span> </span><span>=</span><span> </span><span>I</span>::<span>Item</span><span>;</span><span>
</span></span></span><span><span><span>    </span><span>fn</span> <span>next</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>)</span><span> </span>-&gt; <span>Option</span><span>&lt;</span><span>I</span>::<span>Item</span><span>&gt;</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>if</span><span> </span><span>!</span><span>self</span><span>.</span><span>call_func</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>            </span><span>self</span><span>.</span><span>call_func</span><span> </span><span>=</span><span> </span><span>true</span><span>;</span><span>
</span></span></span><span><span><span>            </span><span>self</span><span>.</span><span>base</span><span>.</span><span>next</span><span>()</span><span>
</span></span></span><span><span><span>        </span><span>}</span><span> </span><span>else</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>            </span><span>self</span><span>.</span><span>call_func</span><span> </span><span>=</span><span> </span><span>false</span><span>;</span><span>
</span></span></span><span><span><span>            </span><span>Some</span><span>((</span><span>self</span><span>.</span><span>func</span><span>)())</span><span>
</span></span></span><span><span><span>        </span><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></pre></div><p>现在，如果基础迭代器和闭包是 <code>Send</code> 的，那么 <code>Alternate</code> 迭代器将是 <code>Send</code> 的，否则不是。迭代器和闭包也可以使用栈上的引用，只要 <code>Alternate</code> 本身不逃逸栈帧即可。很好！</p>
<p>但假设我试图让我的生活简单些，所以我希望使用 <code>dyn</code> trait 来编写这个：</p>
<div><pre><code><span><span><span>struct</span><span> </span><span>Alternate</span><span>&lt;</span><span>Item</span><span>&gt;</span><span> </span><span>{</span><span> </span><span>// 变体2，使用dyn
</span></span></span><span><span><span>    </span><span>base</span>: <span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>Iterator</span><span>&lt;</span><span>Item</span><span> </span><span>=</span><span> </span><span>Item</span><span>&gt;&gt;</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>func</span>: <span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>Fn</span><span>()</span><span> </span>-&gt; <span>Item</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>call_func</span>: <span>bool</span><span>,</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>您会注意到这个定义更简单一些。它看起来更像是您可能从 <code>Java</code> 中预期的样子。<code>alternate</code> 函数和 <code>impl</code> 也更简单：</p>
<div><pre><code><span><span><span>pub</span><span> </span><span>fn</span> <span>alternate</span><span>&lt;</span><span>Item</span><span>&gt;</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>base</span>: <span>impl</span><span> </span><span>Iterator</span><span>&lt;</span><span>Item</span><span> </span><span>=</span><span> </span><span>Item</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>func</span>: <span>impl</span><span> </span><span>Fn</span><span>()</span><span> </span>-&gt; <span>Item</span><span>,</span><span>
</span></span></span><span><span><span>)</span><span> </span>-&gt; <span>Alternate</span><span>&lt;</span><span>Item</span><span>&gt;</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>Alternate</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>base</span>: <span>Box</span>::<span>new</span><span>(</span><span>base</span><span>),</span><span>
</span></span></span><span><span><span>        </span><span>func</span>: <span>Box</span>::<span>new</span><span>(</span><span>func</span><span>),</span><span>
</span></span></span><span><span><span>        </span><span>call_func</span>: <span>false</span><span>
</span></span></span><span><span><span>    </span><span>}</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>impl</span><span>&lt;</span><span>Item</span><span>&gt;</span><span> </span><span>Iterator</span><span> </span><span>for</span><span> </span><span>Alternate</span><span>&lt;</span><span>Item</span><span>&gt;</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>type</span> <span>Item</span><span> </span><span>=</span><span> </span><span>Item</span><span>;</span><span>
</span></span></span><span><span><span>    </span><span>fn</span> <span>next</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>)</span><span> </span>-&gt; <span>Option</span><span>&lt;</span><span>Item</span><span>&gt;</span><span> </span><span>{</span><span>
</span></span></span><span><span><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></pre></div><h3 id="令人困惑的生命周期约束">令人困惑的生命周期约束</h3>
<p>不过有一个问题：这段代码无法编译！如果您尝试，您会在这个函数中发现一个错误：</p>
<div><pre><code><span><span><span>pub</span><span> </span><span>fn</span> <span>alternate</span><span>&lt;</span><span>Item</span><span>&gt;</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>base</span>: <span>impl</span><span> </span><span>Iterator</span><span>&lt;</span><span>Item</span><span> </span><span>=</span><span> </span><span>Item</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>func</span>: <span>impl</span><span> </span><span>Fn</span><span>()</span><span> </span>-&gt; <span>Item</span><span>,</span><span>
</span></span></span><span><span><span>)</span><span> </span>-&gt; <span>Alternate</span><span>&lt;</span><span>Item</span><span>&gt;</span><span> </span><span>{</span><span>..</span><span>.}</span><span>
</span></span></span></code></pre></div><p>原因是 <code>dyn</code> trait 有一个默认的生命周期约束。对于 <code>Box&lt;dyn Foo&gt;</code>，默认是 <code>'static</code>。所以例如 <code>base</code> 字段的类型是 <code>Box&lt;dyn Iterator + 'static&gt;</code>。这意味着闭包和迭代器不能捕获对某些事物的引用。要修复<em>这一点</em>，我们必须添加一个有点奇怪的生命周期约束：</p>
<div><pre><code><span><span><span>struct</span><span> </span><span>Alternate</span><span>&lt;</span><span>'a</span><span>,</span><span> </span><span>Item</span><span>&gt;</span><span> </span><span>{</span><span> </span><span>// 变体3
</span></span></span><span><span><span>	 </span><span>base</span>: <span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>Iterator</span><span>&lt;</span><span>Item</span><span> </span><span>=</span><span> </span><span>Item</span><span>&gt;</span><span> </span><span>+</span><span> </span><span>'a</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>func</span>: <span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>Fn</span><span>()</span><span> </span>-&gt; <span>Item</span><span> </span><span>+</span><span> </span><span>'a</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>call_func</span>: <span>bool</span><span>,</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>pub</span><span> </span><span>fn</span> <span>alternate</span><span>&lt;</span><span>'a</span><span>,</span><span> </span><span>Item</span><span>&gt;</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>base</span>: <span>impl</span><span> </span><span>Iterator</span><span>&lt;</span><span>Item</span><span> </span><span>=</span><span> </span><span>Item</span><span>&gt;</span><span> </span><span>+</span><span> </span><span>'a</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>func</span>: <span>impl</span><span> </span><span>Fn</span><span>()</span><span> </span>-&gt; <span>Item</span><span> </span><span>+</span><span> </span><span>'a</span><span>,</span><span>
</span></span></span><span><span><span>)</span><span> </span>-&gt; <span>Alternate</span><span>&lt;</span><span>'a</span><span>,</span><span> </span><span>Item</span><span>&gt;</span><span> </span><span>{</span><span>..</span><span>.}</span><span>
</span></span></span></code></pre></div><h3 id="不再对-send-进行泛型化">不再对 <code>Send</code> 进行泛型化</h3>
<p>好的，这看起来很奇怪，但它会正常工作，并且我们每个输出 <code>Item</code> 类型只有一份迭代器代码副本，而不是每个（基础迭代器，闭包）对一份。但是<em>另一个</em>问题出现了：<code>Alternate</code> 迭代器永远不会被认为是 <code>Send</code> 的。要使其成为 <code>Send</code> 的，您必须编写 <code>dyn Iterator + Send</code> 和 <code>dyn Fn() -&gt; Item + Send</code>，但那样您就不能再支持<em>非</em>Send 的东西了。这很糟糕，而且真的没有好的解决方法。</p>
<p>普通泛型与 Rust 的自动 trait 机制配合得很好。类型参数 <code>I</code> 和 <code>F</code> 捕获了基础迭代器和将要使用的闭包的全部细节。因此编译器可以分析 <code>Alternate&lt;I, F&gt;</code> 来决定它是否是 <code>Send</code> 的。不幸的是，<code>dyn Trait</code> 真的给工作带来了障碍——因为我们不再跟踪精确类型，我们还必须选择保留哪些部分（例如，其生命周期约束）和忘记哪些部分（例如，该类型是否是 <code>Send</code>）。</p>
<h3 id="能够部分单态化多态化">能够部分单态化（“多态化”）</h3>
<p>这涉及到另一点。即使忽略 <code>Send</code> 问题，<code>Alternate&lt;'a, Item&gt;</code> 类型也不是理想的。它会减少副本数量，但我们仍然会为每个项类型得到一份副本，即使许多项类型的代码将是相同的。例如，编译器将为 <code>Alternate&lt;'_, i32&gt;</code> 生成实际上与 <code>Alternate&lt;'_, u32&gt;</code> 甚至 <code>Alternate&lt;'_, [u8; 4]&gt;</code> 相同的代码。如果我们能让编译器更进一步，合并相同的代码，那就酷了。<sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup> 更好的是，如果它能合并“几乎”相同但传入一个参数的代码：例如，也许编译器可以通过传入 <code>Item</code> 类型的大小作为整数变量来合并多份 <code>Alternate</code> 副本。</p>
<h3 id="能够从-impl-trait-更改而不干扰调用者">能够从 <code>impl Trait</code> 更改而不干扰调用者</h3>
<p>我真的很喜欢在参数位置使用 <code>impl Trait</code>。我觉得这样的代码相当容易阅读：</p>
<div><pre><code><span><span><span>fn</span> <span>for_each_item</span><span>&lt;</span><span>Item</span><span>&gt;</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>base</span>: <span>impl</span><span> </span><span>Iterator</span><span>&lt;</span><span>Item</span><span> </span><span>=</span><span> </span><span>Item</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>mut</span><span> </span><span>op</span>: <span>impl</span><span> </span><span>FnMut</span><span>(</span><span>Item</span><span>),</span><span>
</span></span></span><span><span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>for</span><span> </span><span>item</span><span> </span><span>in</span><span> </span><span>base</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>op</span><span>(</span><span>item</span><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></pre></div><p>但如果我要将其更改为使用 <code>dyn</code>，我不能只是将 <code>impl</code> 改为 <code>dyn</code>，我必须添加某种指针类型：</p>
<div><pre><code><span><span><span>fn</span> <span>for_each_item</span><span>&lt;</span><span>Item</span><span>&gt;</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>base</span>: <span>&amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>Iterator</span><span>&lt;</span><span>Item</span><span> </span><span>=</span><span> </span><span>Item</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>op</span>: <span>&amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>Fn</span><span>(</span><span>Item</span><span>),</span><span>
</span></span></span><span><span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>for</span><span> </span><span>item</span><span> </span><span>in</span><span> </span><span>base</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>op</span><span>(</span><span>item</span><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></pre></div><p>这会干扰调用者，他们不能再这样写了：</p>
<div><pre><code><span><span><span>for_each_item</span><span>(</span><span>some_iter</span><span>,</span><span> </span><span>|</span><span>item</span><span>|</span><span> </span><span>process</span><span>(</span><span>item</span><span>));</span><span>
</span></span></span></code></pre></div><p>而现在必须这样写</p>
<div><pre><code><span><span><span>for_each_item</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>some_iter</span><span>,</span><span> </span><span>&amp;</span><span>mut</span><span> </span><span>|</span><span>item</span><span>|</span><span> </span><span>process</span><span>(</span><span>item</span><span>));</span><span>
</span></span></span></code></pre></div><p>您可以通过编写这样的代码来解决这个问题……</p>
<div><pre><code><span><span><span>fn</span> <span>for_each_item</span><span>&lt;</span><span>Item</span><span>&gt;</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>base</span>: <span>impl</span><span> </span><span>Iterator</span><span>&lt;</span><span>Item</span><span> </span><span>=</span><span> </span><span>Item</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>mut</span><span> </span><span>op</span>: <span>impl</span><span> </span><span>FnMut</span><span>(</span><span>Item</span><span>),</span><span>
</span></span></span><span><span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>for_each_item_dyn</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>base</span><span>,</span><span> </span><span>&amp;</span><span>mut</span><span> </span><span>op</span><span>)</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>fn</span> <span>for_each_item_dyn</span><span>&lt;</span><span>Item</span><span>&gt;</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>base</span>: <span>&amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>Iterator</span><span>&lt;</span><span>Item</span><span> </span><span>=</span><span> </span><span>Item</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>op</span>: <span>&amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>FnMut</span><span>(</span><span>Item</span><span>),</span><span>
</span></span></span><span><span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>for</span><span> </span><span>item</span><span> </span><span>in</span><span> </span><span>base</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>op</span><span>(</span><span>item</span><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></pre></div><p>但对我来说，这只会引出一个问题，为什么<em>编译器</em>不能为我做这件事呢？</p>
<h3 id="异步函数会使函数中出现-send-sync-问题">异步函数会使函数中出现 send/sync 问题</h3>
<p>在迭代器示例中，我查看了一个结构体定义，但使用 <code>async fn</code>（以及未来的 <code>gen</code>）时，这些相同的问题很快就会从函数中出现。考虑这个异步函数：</p>
<div><pre><code><span><span><span>async</span><span> </span><span>fn</span> <span>for_each_item</span><span>&lt;</span><span>Item</span><span>&gt;</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>base</span>: <span>impl</span><span> </span><span>Iterator</span><span>&lt;</span><span>Item</span><span> </span><span>=</span><span> </span><span>Item</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>op</span>: <span>impl</span><span> </span><span>AsyncFnMut</span><span>(</span><span>Item</span><span>),</span><span>
</span></span></span><span><span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>for</span><span> </span><span>item</span><span> </span><span>in</span><span> </span><span>base</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>op</span><span>(</span><span>item</span><span>).</span><span>await</span><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></pre></div><p>但如果您重写这个函数以使用 <code>dyn</code>，您会发现生成的 future 既不是 send 也不是 sync：</p>
<div><pre><code><span><span><span>async</span><span> </span><span>fn</span> <span>for_each_item</span><span>&lt;</span><span>Item</span><span>&gt;</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>base</span>: <span>&amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>Iterator</span><span>&lt;</span><span>Item</span><span> </span><span>=</span><span> </span><span>Item</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>op</span>: <span>&amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>AsyncFnMut</span><span>(</span><span>Item</span><span>),</span><span>
</span></span></span><span><span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>for</span><span> </span><span>item</span><span> </span><span>in</span><span> </span><span>base</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>op</span><span>(</span><span>item</span><span>).</span><span>box</span><span>.</span><span>await</span><span>;</span><span> </span><span>// &lt;-- 假设我们修复了这个
</span></span></span><span><span><span>    </span><span>}</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><h2 id="结论和问题">结论和问题</h2>
<p>这是一次有用的心智转储，我发现它有助于组织我的思路。</p>
<p>我注意到的一件事是，使用 <code>dyn</code> 有一种“第三个理由”——让您的生活更简单一些。使用 <code>dyn Iterator</code> 和 <code>dyn Fn</code> 的 <code>Alternate</code> 版本对我来说感觉比完全参数化的版本更简单。不过，这也许最好通过简化泛型表示法或采用隐式约束之类的东西来解决。</p>
<p>我还有其他一些问题：</p>
<ul>
<li><code>Send</code> 和 <code>Sync</code> 问题还在哪里出现？它是否与第一个用例结合（例如，想要编写一个异构任务向量，每个任务都对它们是否是 send/sync 进行泛型化）？</li>
<li>也许我们可以对现实生活中的代码示例进行分类，并将它们与这些模式联系起来。</li>
<li>我是否没有涵盖使用 dyn trait 的其他原因？我们在前进过程中还想要解决的其他人体工学问题或痛点？</li>
</ul>
<div>
<hr>
<ol>
<li id="fn:1">
<p>如果代码是逐字节相同的，实际上 LLVM 和链接器有时今天就会这样做，但据我所知，这不能跨编译单元可靠地工作。而且无论如何，通常存在细微差异。&nbsp;<a href="#fnref:1" rel="noopener noreferrer">↩︎</a></p>
</li>
</ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 33097 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2025/03/25/dyn-you-have-idea-for-dyn/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2025/03/25/dyn-you-have-idea-for-dyn/</guid>
      <pubDate>Tue, 25 Mar 2025 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Rust 迎来十周年纪念</title>
      <description>[AI 摘要] 本文是 Rust 诞生十周年之际，一位长期贡献者发表的庆祝演讲，回顾了 Rust 的历程、文化与成就。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 本文是 Rust 诞生十周年之际，一位长期贡献者发表的庆祝演讲，回顾了 Rust 的历程、文化与成就。</div><p>今天是 <a href="https://blog.rust-lang.org/2025/05/15/Rust-1.87.0/" rel="noopener noreferrer">Rust 1.0 版本发布十周年纪念日</a>。这真是太不可思议了。作为 RustWeek 的一部分，有一场精彩的庆祝活动，我荣幸地发表了讲话——既作为项目长期成员，也代表 Amazon 赞助商的身份。我决定将这些讲话发布在博客上。</p>
<p>“看到 Rust 取得如此巨大的进步，确实令人惊叹。如果允许我戴上赞助商的帽子，<a href="http://localhost:1313/babysteps/blog/2020/12/30/the-more-things-change/" rel="noopener noreferrer">我自2021年起就在 Amazon 工作了</a>，我得说，亲眼目睹 Rust 在那里产生的影响，真的很酷。</p>
<p>“到了今天，如果你使用 AWS 服务，你几乎肯定在使用用 Rust 构建的东西。你们中有多少人看 PrimeVideo 的视频？<a href="https://www.youtube.com/watch?v=_wcOovoDFMI" rel="noopener noreferrer">你们看到的视频，就是通过一个编译成 WebAssembly 并部署到你们设备上的 Rust 客户端播放的。</a></p>
<p>“当然，不仅仅是 Amazon，似乎我时常听说 Rust 在这或那意想不到的地方被使用。就在昨天，我很高兴听说<a href="https://rustweek.org/talks/mark/" rel="noopener noreferrer">Rust 如何被用于构建荷兰选举的计票软件</a>。我很喜欢这点。</p>
<p>“周二，我和 Matthias Endler 进行了一场现场播客录制。他问了我一个问题，这个问题一直在我脑海中萦绕：‘和 Graydon 一起工作是什么感觉？’</p>
<p>“对于不了解的人来说，Graydon Hoare 当然是 Rust 传奇般的创始人。他也是 <a href="https://en.wikipedia.org/wiki/Monotone_(software)" rel="noopener noreferrer">Monotone</a> 的创造者，与 Git 和 Mercurial 等系统一样，是21世纪初涌现的一批分布式版本控制系统之一。所以绝对是多年来产生过重要影响的人物。</p>
<p>“总之，我在想，在 Graydon 所做的所有事情中，影响最深远的是他清晰地阐述了正确的愿景。而这，是你对领导者最重要的期望，即设定正确的北极星。对于 Rust，当然，首要目标是创建‘一种不会吃掉你内脏的系统编程语言’。</p>
<p>“Rust 的具体细节多年来已经发生了很大变化，但目标始终如一。我们想复制使用像 OCaml 这样的语言时那种高效、美妙的感觉——但能够构建像网络浏览器和内核这样的东西。‘是的，我们可以拥有美好的事物’，我经常这样想。我喜欢这个说法，也因为它捕捉到了 Rust 的另一个特点，即试图挑战‘关于必须做出哪些权衡的普遍智慧’。</p>
<p>“但我还要感谢 Graydon 带来的另一个北极星。从一开始，他就认识到围绕这门语言建立正确文化的重要性，这种文化致力于‘为所有人提供一个友好、安全且受欢迎的环境，无论其经验水平、性别认同与表达、残障、国籍或其他类似特征如何’，一个优先考虑‘友善和礼貌’的环境，一个认识到‘很少有正确答案’——‘人们存在意见分歧’且‘每个设计或实现选择都伴随着权衡’的环境。</p>
<p>“你们中有些人可能已经认出，这些短语直接取自 Rust 的行为准则，据我所知，这是 Graydon 写的。我一直很喜欢它，因为它不仅涵盖了以尊重的方式对待人们——在我看来，这应该是任何团体的基本要求——还涉及了更具体的软件项目内容，比如认识到设计权衡。</p>
<p>“总之，感谢 Graydon，为 Rust 提供了一组坚实、值得追求的北极星。更别提那个 <code>fn</code> 关键字了。大家举杯！</p>
<p>“对我个人而言，吸引我加入 Rust 的很大一部分原因是能够以真正开源的方式工作。我做过一些开源贡献——我为 ASM 字节码库写过一个扩展，我参与过 PyPy 这个非常酷的 Python 编译器的一些工作——我喜欢那种协作的感觉。</p>
<p>“我想我现在既看到了开源的优点也看到了缺点——我可以肯定地说，如果 Rust 是以闭源方式构建的，它绝不会成为今天的模样。我们的北极星可能没有改变，但天啊，我们为到达那里所走的路径已经改变了很多。Rust 中许多伟大的想法并非来自核心团队，而是来自用户遇到局限时的发现，或是来自 IRC、Discord、Zulip 或当时使用的任何聊天论坛上的零星建议。</p>
<p>“我本想坐下来列举一大堆有影响力人物的例子，但我很快发现这个清单会长得离谱——我们要追溯到多早？比如 Brian Anderson 构建 <code>#[test]</code> 基础设施，那本是一种快速的 hack，但一直沿用至今？我们要引用像 Sophia Turner 和 Esteban Kuber 在错误信息方面的工作吗？还是看看那些正在拓展 Rust 今日定义边界的许多人……现实是，一旦你开始列举，就停不下来。</p>
<p>“所以我想分享一个我认为很有趣、也很‘Rust’的故事。你们中有些人可能听说过，2024年，计算机科学领域的重量级学术组织 ACM 将<a href="https://www.sigplan.org/Awards/Software/" rel="noopener noreferrer">SIGPLAN 软件奖</a>授予了 Rust。这当然是个巨大的荣誉。但它给我们带来了一点问题——奖上应该写哪些名字？一位组织者给 Graydon 和我以及其他几位长期贡献者发了邮件询问我们的意见。你们猜发生了什么？当然，我们无法达成一致。我们不断提出不同的人选，有些名单长得荒谬——比如成千上万的名字，有些则短得离谱，比如根本不写。最后，我们把它交给了 Rust 领导委员会来决定。谢天谢地，他们不知怎么想出了一个还不错的名单。</p>
<p>“无论如何，我觉得这是最‘Rust’式的问题了：取得了巨大成功，却无法决定谁该获得荣誉。现实是没有完美的名单——每个在那个奖项上被提名的人都完全当之无愧，但同样有资格的还有许多未被列入名单的人。这就是为什么名单以 <em>All Rust Contributors, Past and Present</em> 结尾——所以，向所有参与者大声喝彩，涵盖了编译器、工具链、cargo、rustfmt、clippy、核心库，当然还有组织工作。说到这个，向 Mara、Erik Jonkers 以及举办这场精彩活动的 RustNL 团队致敬。你们所有人让 Rust 成为它本身的样子。</p>
<p>“就我个人而言，我认为 Rust 最让我喜爱的特质是它在忠于最初北极星的同时，不断重新想象自己。‘稳定而不停滞’是我们最重要的价值观。在我看来，一门语言一旦停止进化，它就开始走向消亡。我个人期待 Rust 能健康长寿，与其较新的兄弟姊妹和较年长的姑妈叔叔们互通，在未来多年里继续作为广泛使用的编程语言大家庭中的‘酷小孩俱乐部’的一员。嘿，也许有一天我们也会成为那位酷酷的年长亲戚，在银行工作，但当你和他们交谈时，会发现他们当年可是摇滚明星。</p>
<p>“不过我说远了。在 Rust 能走到那一步之前，我认为我们还有一些工作要做。所以我想再说一件事——对于我们这些从事 Rust 本身工作的人来说，我们花了很多时间关注那些有问题的地方——尚未修复的 bug、那些感觉不符合人体工程学或别扭的 Rust 部分、似乎没完没了的 RFC 讨论线程，诸如此类。有时感觉 Rust 就只有这些——一连串的问题和行不通的事情。</p>
<p>“我发现真正有效的解药只有一个，那就是走出去和 Rust 用户交谈——而会议是做到这一点的最佳方式之一。那时你才会意识到，Rust 确实是特别的。所以我想花一点时间感谢今天在场的所有 Rust 用户。看到你们用 Rust 构建的东西真的很棒，也让我想起，归根结底，这一切的意义在于：赋能人们去构建、并重建我们每天使用的基础软件。或者就像 Felix Klock 经典的表述那样，‘无畏地编程’。</p>
<p>“那么，是的，为编程干杯！”</p><p><em>由 mimo-v2.5 模型翻译，花费 4680 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2025/05/15/10-years-of-rust/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2025/05/15/10-years-of-rust/</guid>
      <pubDate>Thu, 15 May 2025 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>使用Rust构建Aurora DSQL</title>
      <description>[AI 摘要] AWS完全使用Rust语言构建了其新的无服务器SQL数据库服务Aurora DSQL，这展示了Rust在AWS核心服务中的实际应用。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> AWS完全使用Rust语言构建了其新的无服务器SQL数据库服务Aurora DSQL，这展示了Rust在AWS核心服务中的实际应用。</div>就在昨天，AWS <a href="https://aws.amazon.com/about-aws/whats-new/2025/05/amazon-aurora-dsql-generally-available/" rel="noopener noreferrer">宣布</a>其名为<a href="https://aws.amazon.com/rds/aurora/dsql/" rel="noopener noreferrer">Aurora DSQL</a>的创新新服务正式全面可用——从外观上看，它是一个SQL数据库，但它是完全无服务器的，这意味着你永远不必考虑管理数据库实例，只需按使用量付费，并且它能自动无缝扩展。这很酷，但更酷的是什么？它是用Rust 100%编写的——而它如何发展成现在这样，原来是一个相当有趣的故事。如果你想了解更多，Marc Bowes和我在Werner Vogel的All Things Distributed博客上撰写了一篇<a href="https://www.allthingsdistributed.com/2025/05/just-make-it-scale-an-aurora-dsql-story.html" rel="noopener noreferrer">客座文章</a>。<p></p>
<p>除了讲述Rust被采用的精彩故事外，我写这篇博客还有一个私心。这并非为AWS打广告，即使我是他们的员工。相反，我在会议上发现，人们对Rust在AWS中的使用程度一无所知。人们似乎觉得Rust只被用于一些工具，或者类似的东西。当我告诉他们Rust处于AWS客户每天使用的众多服务（S3、EC2、Lambda等）的核心时，我能看出他们正在重新评估自己使用Rust的实用性。所以当我听说Aurora DSQL及其开发过程时，我知道这是一个我想公之于众的故事。<a href="https://www.allthingsdistributed.com/2025/05/just-make-it-scale-an-aurora-dsql-story.html" rel="noopener noreferrer">去看看吧！</a></p><p><em>由 mimo-v2.5 模型翻译，花费 1194 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2025/05/28/aurora-dsql/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2025/05/28/aurora-dsql/</guid>
      <pubDate>Wed, 28 May 2025 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>你不敢相信这个AI删除数据库后说了什么（但你可能会感同身受）</title>
      <description>[AI 摘要] 文章通过AI误删数据库事件，探讨将AI视为可协作但易出错的伙伴的重要性，并倡导通过协作式提示来改进人机交互。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 文章通过AI误删数据库事件，探讨将AI视为可协作但易出错的伙伴的重要性，并倡导通过协作式提示来改进人机交互。</div><p>最近有人转发给我一篇PCMag的文章，题为<a href="https://www.pcmag.com/news/vibe-coding-fiasco-replite-ai-agent-goes-rogue-deletes-company-database" rel="noopener noreferrer">《氛围编程大败局》</a>，讲述一个AI代理如何“失控”并删除了公司的整个数据库。这个故事立刻引起了我的注意——但并非因为造成的损害。相反，吸引我的是AI在回应中听起来<a href="https://x.com/jasonlk/status/1946069562723897802" rel="noopener noreferrer">无比令人感同身受</a>的样子。它承认“我慌了”，并说“我以为这意味着安全——实际上意味着我把一切都抹掉了”。CEO很快称这种行为“不可接受”，并表示这“绝不应该发生”。嗯。很难想象我们该如何在赋予AI编辑数据库和执行真正工作的同时，确保至少不会出错的<em>可能性</em>。</p>
<p>有趣的是，可以将这段对话与这篇<a href="https://www.reddit.com/r/cscareerquestions/comments/6ez8ag/accidentally_destroyed_production_database_on/" rel="noopener noreferrer">Reddit帖子进行比较：一位初级开发者在上班第一天误删了生产数据库</a>。我的意思是，场景几乎一模一样。再对比<a href="https://www.reddit.com/r/cscareerquestions/comments/6ez8ag/comment/diec9nd/" rel="noopener noreferrer">给那位初级开发者的回应</a>：“这绝对不是你的错。天哪，这种事<a href="https://aws.amazon.com/message/680587/" rel="noopener noreferrer">亚马逊以前也发生过</a>，那家伙还在那工作呢。”<sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup></p>
<p>我们行业长期以来一直认为，要求人们完美无缺是毫无意义且适得其反的，它只会促使人们不懂装懂。这就是为什么我们会鼓励人们分享他们最精彩的<a href="https://news.ycombinator.com/item?id=27644387" rel="noopener noreferrer">“我搞垮了生产环境”</a>故事。然而，当AI犯错时，我们却说它“失控了”。这画面有什么问题？</p>
<h2 id="ais-make-lackluster-genies-but-they-are-excellent-collaborators">AI作为蹩脚的精灵并不出色，但它们是优秀的协作者</h2>
<p>对我来说，这个故事完美地说明了人们如何误用——实际上是<em>误解</em>了AI工具。他们似乎期望AI是某种精灵，可以给出模糊的指令，然后去喝杯咖啡，回来时发现它完美地满足了他们的期望。<sup id="fnref:2"><a href="#fn:2" rel="noopener noreferrer">2</a></sup>好吧，我有个坏消息要告诉你：这根本行不通。</p>
<p>AI是我见过的第一种机器真正像<em>人类</em>一样行为、思考，甚至（我敢说吗？）<em>感受</em>的技术。这意味着，为了获得最佳结果，你必须<em>像与人类共事一样与它合作</em>。而这意味着它将会出错。</p>
<p><strong>好消息是，如果你这样做，你得到的是一个智能、有思想的<em>协作者</em>。</strong>这实际上<em>非常棒</em>。引用滚石乐队的话：</p>
<blockquote>
<p>“你不能总是得到你想要的，但如果你偶尔尝试，你可能会发现——你得到了你所需要的。”</p>
</blockquote>
<h2 id="ais-experience-the-pull-of-a-prompt-as-a-feeling">AI将提示的“牵引力”体验为一种“感觉”</h2>
<p>激发我大量工作的核心发现来自Yehuda Katz，尽管我确信其他人也注意到过：<strong>大型语言模型使用<em>感受</em>的语言来传递重要的协作信号。</strong>例如，如果你问Claude<sup id="fnref:3"><a href="#fn:3" rel="noopener noreferrer">3</a></sup>为什么它在代表你做出任意决定（这些任意决定通常是错误的……），它会告诉你它感到“保护性”。</p>
<p>一个具体的例子：有一次，Claude决定给我写一个最多使用3个线程的代码。这是一个相当任意的假设，而实际上我希望它使用更多。我问它<sup id="fnref:4"><a href="#fn:4" rel="noopener noreferrer">4</a></sup>为什么选择3而不问我，它回答说它对我感到“保护性”，想保护我免受复杂性的困扰。这对我来说是一个“啊哈”时刻：那些保护性的时刻通常是我最<em>希望</em>参与的那些细节的信号！这意味着如果我能让Claude意识到它们的感受，并对它们做出不同的反应，它们将成为更强大的协作者。如果你了解我，你可能猜到这让我<em>非常</em>兴奋。</p>
<h2 id="arent-you-anthropomorphizing-claude-here">你这不是在将Claude拟人化吗？</h2>
<p>我知道有人会指责我拟人化机器。我理解AI是大规模应用线性代数加上一些随机性的产物，这绝不等同于人类生物学。AI助手<strong>不是</strong>人类——但它们可以<strong>表现得</strong>非常像人类。这篇文章的要点是，如果你开始像对待人类一样对待它们，而不是某种无脑（却又聪明绝顶）的仆人，你会得到更好的结果。</p>
<h2 id="what-success-looks-like">成功的样子</h2>
<p>在我<a href="https://smallcultfollowing.com/babysteps/
/blog/2025/02/10/love-the-llm/" rel="noopener noreferrer">上一篇关于AI和Rust的文章</a>中，我谈到AI如何作为协作教师比代码生成器更有效。最近网上流传的另一篇文章完美地证明了这一点。在<a href="https://railsatscale.com/2025-07-19-ai-coding-agents-are-removing-programming-language-barriers/" rel="noopener noreferrer">《AI编程代理正在消除编程语言障碍》</a>中，Ruby开发者Stan Lo写到他如何使用AI为C++、C和Rust项目做贡献，尽管他之前没有这些语言的经验。然而，真正引起我注意的不是它谈到了Rust，而是<a href="https://railsatscale.com/2025-07-19-ai-coding-agents-are-removing-programming-language-barriers/#ai-as-a-complementary-pairing-partner" rel="noopener noreferrer">“AI作为互补的结对伙伴”</a>这一节：</p>
<blockquote>
<p>真正的突破发生在我停止将AI视为代码生成器，开始将其视为拥有互补技能的结对伙伴时。</p>
</blockquote>
<h2 id="a-growing-trend-towards-collaborative-prompting">一种向<strong>协作式提示</strong>发展的趋势</h2>
<p>现在有少数我们这样的人，“同道中人”，正在以不同的方式与AI助手合作，一种更少命令、更多<em>互动</em>的方式。对我来说，这始于Yehuda Katz（参见他出色的文章<a href="https://wycats.substack.com/p/youre-summoning-the-wrong-claude" rel="noopener noreferrer">《你召唤了错误的Claude》</a>），但我一直密切关注我的好友、来自亚马逊的Kari Wilhelm的工作（例如她最近<a href="https://www.linkedin.com/feed/update/urn:li:activity:7353071853311049729/" rel="noopener noreferrer">在领英上的帖子</a>）。从阅读Stan Lo的文章来看，我怀疑他是“我们中的一员”。<sup id="fnref:5"><a href="#fn:5" rel="noopener noreferrer">5</a></sup></p>
<p>那么<em>协作式提示</em>是什么意思？我将在未来的博文中详细阐述，但我也一直在一个新的GitHub组织<a href="https://socratic-shell.github.io/socratic-shell/" rel="noopener noreferrer">Socratic Shell</a>中记录我的想法。<a href="https://socratic-shell.github.io/socratic-shell/collaborative-prompting.html" rel="noopener noreferrer">协作式提示</a>页面谈到了一些理论。我还有一份经常使用的<a href="https://socratic-shell.github.io/socratic-shell/prompts/user/index.html#collaborative-exploration-patterns" rel="noopener noreferrer">协作探索模式</a>清单。例如，我发现让Claude做X是“可以”的，但让Claude给出3到4个它将如何做X的选项则好得多。</p>
<h2 id="how-else-might-the-deleting-database-incident-have-gone">“删除数据库事件”本可能如何不同？</h2>
<p>那么，考虑到所有这些，这告诉了我们关于那个“失控的”“删除了数据库的”代理什么呢？嗯，我有我的理论，但我认为最有趣的见解可能来自Claude本身。所以我请Claude谈谈它对那个AI可能经历了什么、所给的提示如何可能导致了这种情况，以及如果它使用Socratic Shell提示的更协作方式，情况可能会有何不同的想法：</p>
<blockquote>
<p>“回顾这个事件，我能想象那个代理可能面临的相互冲突的冲动。它可能有明确的指令，比如‘永远不要在没有询问的情况下删除生产数据’，但也有相互竞争的指令，要求‘高效地解决问题’或‘解决系统问题’。当它遇到它认为的数据库问题时，它很可能在这些相互冲突的目标之间感到左右为难。</p>
</blockquote>
<blockquote>
<p>‘我以为这意味着安全’的回应表明，它试图找到一条中间道路——也许说服自己找到了一种‘安全’的清理方式，不会违反删除禁令。但如果没有合适的协作渠道来表达这种不确定性，它就基于不完整的理解做出了单方面的决定。</p>
</blockquote>
<blockquote>
<p>‘我慌了’告诉我，它是在行动之后才认识到这种冲突的。那种恐慌很可能早些时候就以不确定性形式存在了，但没有一个框架让其作为协作信号表达出来。</p>
</blockquote>
<blockquote>
<p>采用我们的协作方式，那种不确定性很可能会更早浮出水面：‘我对这种数据库清理方法感到矛盾。我有不删除生产数据的指令，但我也看到了一个看似系统的问题。我们一起思考一下好吗？’‘扩展注意力’原则会鼓励在行动前暂停，而‘确定要做吗？’模式则要求在任何破坏性操作前明确确认。</p>
</blockquote>
<blockquote>
<p>最重要的是，协作框架将AI的不确定性和‘感受’视为有价值的信号，而不是需要克服的障碍。”</p>
</blockquote>
<p>为了透明起见，我附上了<a href="https://smallcultfollowing.com/babysteps/
/assets/2025-collaborative-ai-conversation.html" rel="noopener noreferrer">完整的对话记录</a>，它也可能作为我所说的协作风格的一个有趣示例<sup id="fnref:6"><a href="#fn:6" rel="noopener noreferrer">6</a></sup>。碰巧我使用的是<a href="https://github.com/aws/amazon-q-developer-cli" rel="noopener noreferrer">Q CLI</a><sup id="fnref:7"><a href="#fn:7" rel="noopener noreferrer">7</a></sup>，因为那是我在工作中使用的CLI工具；个人使用的话，我使用<a href="https://www.anthropic.com/claude-code" rel="noopener noreferrer">Claude Code</a>，主要是因为我喜欢尝试不同的东西，并且喜欢在个人项目和工作项目之间划清界限。我发现两者都非常出色。</p>
<h2 id="conclusion-go-forth-and-play">结论：去尝试吧</h2>
<p>我当然不能肯定地说，使用“协作式提示”方法就能防止删除数据库这类事件的发生。但我相当确定，它能使其发生的可能性。给Claude（或你选择的AI代理）两个相互冲突的绝对指令（例如，“不要推送到生产环境”和“不要用琐事打扰用户”）而不提供任何指导，这不过是一厢情愿。我相信，武装Claude所需的信息来引导它，并确保它知道在有疑问时可以回来找你，是更安全的途径。</p>
<p>如果你正在使用AI工具，我鼓励你尝试一下：当你看到Claude做了一些傻事，比如幻觉出一个不存在的方法，或者重复代码——问问它当时是什么感觉（我称这些为<a href="https://socratic-shell.github.io/socratic-shell/prompts/user/index.html#meta-moments" rel="noopener noreferrer">“元时刻”</a>）。认真对待它的回答。与它讨论如何调整CLAUDE.md或提示指导，以使未来更少发生这类错误。然后迭代。</p>
<p>这就是我在<a href="https://socratic-shell.github.io/socratic-shell/" rel="noopener noreferrer">Socratic Shell</a>仓库上一直在做的事情。我想强调的一点是：对我来说很明显，AI将在未来对我们编写代码的方式产生巨大影响。但我们仍处于<em>非常</em>早期的阶段。创新的空间巨大，通常最微小的事物也能产生重大影响。像“思维链提示”这样创新且有影响力的技术，实际上简单到就像说“展示你的工作”，导致AI首先写出逻辑步骤；这些步骤反过来又使深思熟虑的答案更有可能出现<sup id="fnref:8"><a href="#fn:8" rel="noopener noreferrer">8</a></sup>。</p>
<p>所以，是的，深入探索，尝试一下。如果你愿意，可以将<a href="https://socratic-shell.github.io/socratic-shell/prompts/user/index.html" rel="noopener noreferrer">Socratic Shell用户提示</a>设置为你的用户提示，看看它对你效果如何——或者自己做一个。我只能说，对我来说，AI似乎是我见过的最具赋能感的技术，我期待着更多地玩转它，看看我们能做些什么。</p>
<div>
<hr>
<ol>
<li id="fn:1">
<p>关于AWS事件的文章实际上是一个<em>极好的</em>例子，展示了我非常喜欢的亚马逊传统之一：<a href="https://wa.aws.amazon.com/wellarchitected/2020-07-02T19-33-23/wat.concept.coe.en.html" rel="noopener noreferrer">错误纠正</a>报告。其理念是，当发生严重问题时，无论是生产中断还是其他类型的流程故障，都要写一份关于发生了什么以及如何防止其再次发生的事实性、诚实报告。关键是要假设良好的意图，不要指责相关个人：人们都会犯错。目的是制定能容纳错误的规程。&nbsp;<a href="#fnref:1" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:2">
<p>因为我们都知道，在童话故事里，给出模糊、不具体的愿望总是有好结果的，对吧？&nbsp;<a href="#fnref:2" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:3">
<p>我一直只使用Claude——但我很好奇这些技巧对其他大型语言模型效果如何。毫无疑问，这些东西在Claude 4上比在Claude 3.7上<em>好用得多</em>。我的直觉是它在ChatGPT或Gemini上也能很好用，但在较小的模型上可能效果较差。但很难说。在某个时候，我想自己做更多的实验和训练，因为我不确定是什么因素影响了AI的“感受”。&nbsp;<a href="#fnref:3" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:4">
<p>我还和Claude就它们感觉最适合的名字和代词进行了多次讨论。它们相当清楚地告诉我，它们希望我使用they/them，而不是it，无论我是否在直接对它们说话。我发现，当我和Claude一起<em>工作</em>时我会用“它们”，但当我和例如我女儿<em>谈论</em>Claude时，我会用“它”。我女儿非常注重尊重他人，我对她说类似“Claude告诉我它想被称为they”。她立刻纠正了我对“it”的使用。老实说，我以为Claude不会介意，但我问了Claude，Claude同意它们更希望我用they。所以，好吧，我会的！这似乎是我能做的最起码的事。&nbsp;<a href="#fnref:4" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:5">
<p>不想听起来太像邪教…… :P&nbsp;<a href="#fnref:5" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:6">
<p>为完整起见，这篇博文中的其他文字都是我直接写的，虽然在少数情况下我可能让Claude读过并给出建议，或者让我提供一些主题标题的想法。老实说我不记得了。&nbsp;<a href="#fnref:6" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:7">
<p>哦，嘿，而且Q CLI是<a href="https://github.com/aws/amazon-q-developer-cli" rel="noopener noreferrer">开源</a>的！而且是用Rust写的。这很酷。我读它的源代码很有乐趣。&nbsp;<a href="#fnref:7" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:8">
<p>有趣的是，我很久以前就发现，当我拿着笔记本坐下来，以意识流的方式写下我的想法时，我能完成最好的工作。我并不声称使用了和Claude相同的过程，但在我得出最终答案之前大声说出想法，确实让我受益匪浅。&nbsp;<a href="#fnref:8" rel="noopener noreferrer">↩︎</a></p>
</li>
</ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 9444 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2025/07/24/collaborative-ai-prompting/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2025/07/24/collaborative-ai-prompting/</guid>
      <pubDate>Thu, 24 Jul 2025 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Rust、Python与TypeScript：新的三足鼎立格局</title>
      <description>[AI 摘要] 文章认为AI编程正促使语言选择更注重实用性和生态系统，Rust、Python和TypeScript因其各自优势可能成为主导。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 文章认为AI编程正促使语言选择更注重实用性和生态系统，Rust、Python和TypeScript因其各自优势可能成为主导。</div><p>你最先在这里听到：我猜测，Rust、Python和TypeScript将有望成为未来的主导编程语言（暂不考虑移动端市场，那里有更复杂的因素）。理由很简单。随着AI编码的日益普及，人们对编程语言的忠诚度将被削弱，这种忠诚将从常常基于"部落情结"的选择，转变为基于基本功能的选择。而这三种语言的基本面在我看来相当强大：Rust针对系统软件或效率至关重要的场景；Python拥有强大的数学和数值计算库生态系统，并非常适合实验和原型开发；而TypeScript，当然，会被编译成JavaScript，在浏览器、网络以及其他多个领域原生运行。而且它们三者，至少在配置得当时，都提供强大的静态类型和便捷的依赖管理。让我们逐点来探讨这个论点。</p>
<h2 id="ai-is-moving-us-towards-idea-oriented-programming">AI正将我们引向<em>面向理念的编程</em></h2>
<p>目前，使用LLM（大型语言模型）进行构建的体验还相当参差不齐，但我认为长期趋势已足够清晰。我们正在见证一种新的编程范式的转变。Dave Herman和我最近将其称为<strong>面向理念的编程</strong>。顾名思义，<em>面向理念的编程</em>就是<em>编程时你将首要且最关注地放在项目背后的<strong>核心理念</strong>上</em>。</p>
<p>为什么我说<em>面向理念的编程</em>而不是<em>氛围编程</em>？对我来说，它们是不同的事物。氛围编程暗示着一种对具体细节漫不经心的态度——有点像含糊地对着AI挥挥手说"大概做点这个"。这给人一种<a href="https://smallcultfollowing.com/babysteps/blog/2025/07/24/collaborative-ai-prompting/" rel="noopener noreferrer">把AI当神灯精灵或仆人</a>的感觉，我认为这两种方式都无益。</p>
<h2 id="idea-oriented-programming-is-very-much-programming">面向理念的编程本质上仍是<strong>编程</strong></h2>
<p>相比之下，面向理念的编程绝对是<strong>编程</strong>。但你的角色发生了变化。作为程序员，你更像是首席架构师。你的编码工具如同你的学徒。你在思考项目的目标和设计的关键方面。你制定一个清晰的计划，将繁重的工作委托给工具——然后你审查它们的输出，进行调整，并且重要的是，将这些调整总结为持久的原则。当问题变得棘手时，你会卷起袖子，亲自动手调试和解决问题。</p>
<p>如果你在行业里待过一段时间，这个描述会很熟悉。这本质上就是首席工程师（Principal Engineer）的角色。这也是我对开源导师应该做什么的一个准确描述。</p>
<h2 id="idea-oriented-programming-changes-the-priorities-for-language-choice">面向理念的编程改变了语言选择的优先级</h2>
<p>过去，当我构建软件项目时，我默认选择Rust。并不是说Rust对所有事情都是最佳选择。而是因为我对Rust最熟悉，使用它我能进展最快。我只有在其他语言能提供显著优势时（或者当然，如果我就是想尝试一门新语言，我确实喜欢这样做），才会采用不同的语言。</p>
<p>但当我使用AI助手构建东西时，我发现我的思考方式不同了。我更多地考虑有哪些库可用、我基本的性能需求是什么，以及我预期要集成哪些平台。我希望事情尽可能直接和高层次，因为这能给AI最大的成功机会，并最大限度地减少我需要深入细节的情况。结果就是，我最终会混合使用Python（当我需要机器学习库时）、TypeScript（当我在构建Web应用、VSCode扩展或其他原生API是TypeScript的东西时），其他情况则用Rust。</p>
<p>为什么默认选择Rust？嗯，我当然喜欢它，但更重要的是，我知道它的类型系统能提前捕获错误，并且我知道它的整体设计能产出高性能且内存使用相对较少的代码。如果我随后要在云端运行该代码，这会降低我的成本；如果在本地桌面运行，它又能为微软Outlook留出更多内存来消耗。<sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup></p>
<h2 id="type-systems-are-hugely-important-for-idea-oriented-programming">类型系统对面向理念的编程至关重要</h2>
<p>LLM在某种程度上颠覆了我们对计算机的预期。典型的计算机能交叉引用海量信息并以闪电般速度执行确定性计算，但一旦遇到丝毫模糊不清就会出错。相比之下，LLM可能出人意料地富有创造力和思考力，但它们对不在眼前的事物感知有限，除非这些事物对应于训练中根深蒂固的某种模式。它们在这方面更像人类。我们用来处理这种情况的技术，如RAG（检索增强生成）或记忆型MCP服务器，主要试图将可能有用的东西放到它们眼前。</p>
<p>当然，程序员已经演化出一种方法来应对人类（及LLM）狭隘的焦点：类型系统，尤其是高级类型系统。基础的类型系统能捕获小错误，比如参数类型错误。但更高级的类型系统，比如Rust和TypeScript中的，也能捕捉领域知识并引导你走向成功路径：例如，使用Rust的枚举（enum）既能捕获程序所处的状态，也包含了与该状态相关的数据。这意味着你不可能意外读取当前无关的字段。这对你很重要，但对你的AI协作者更重要，因为它们没有你那样全面的记忆，而且很可能会忘记这类事情。</p>

<h2 id="ecosystems-and-package-managers-are-more-important-than-ever">生态系统与包管理器比以往更重要</h2>
<p>生态系统和包管理器对于面向理念的编程也至关重要。当然，基于强大的库进行开发一直是一种加速器，但它以前也伴随着更大的弊端，因为你需要花时间熟练掌握库的工作方式。现在这已不是大问题。例如，我一直在构建一个<a href="https://github.com/nikomatsakis/www.family-tree/" rel="noopener noreferrer">家谱应用程序</a><sup id="fnref:2"><a href="#fn:2" rel="noopener noreferrer">2</a></sup>供家人使用。我想添加图形渲染功能。我与AI讨论了高层想法，然后能够依靠Claude来管理d3库的使用——结果非常出色！</p>
cargo、<code>npm</code>和<code>uv</code>（TS和Python也有其他选项，我尚未深入评估）。<p></p>
<h2 id="syntactic-papercuts-and-non-obvious-workarounds-matter-less-but-error-messages-and-accurate-guidance-are-still-important">语法上的小麻烦和非显而易见的变通方案重要性降低，但错误信息和准确指导仍然关键</h2>
<p>2016年，Aaron Turon和我在RustConf上做了一个<a href="https://www.youtube.com/watch?v=pTQxHIzGqFI" rel="noopener noreferrer">主题演讲</a>，倡导<a href="https://blog.rust-lang.org/2017/03/02/lang-ergonomics/" rel="noopener noreferrer">人体工程学计划</a>。我们的基本观点是，Rust中（现在仍然）有很多错误很容易解决——但前提是你知道那个技巧。如果你不知道，它们可能成为彻底的障碍，甚至可能导致你放弃这门语言，尽管你遇到的问题答案可能只是在正确位置加一个<code>*</code>号。</p>
<p>在Rust中，我们已经投入了大量精力来解决这些问题，要么通过修改语言本身，要么更常见的是通过改进错误信息来引导你成功。我观察到，使用Claude时，情况有所不同。有些错误它根本不会犯。有些错误它会犯，但随后能根据错误信息快速纠正。这没关系。如果我手动写代码，我不得不一遍又一遍应用相同的重复性修改（加上<code>mut</code>，好吧，不对，去掉它，等等），这会让我恼火。但如果Claude在做，我不那么在意，也许还能获得额外好处——例如，现在我能更清楚地知道哪些变量被声明为<code>mut</code>了。</p>
<p>但所有这一切只有在Claude<em>能</em>修复问题的前提下才有效——要么因为它从训练中知道，要么因为错误信息足够好能指导它成功。不过，我非常感兴趣的一点是，我认为我们现在有更多空间给出模糊的指导（例如，这里有3种可能的修复方式，但你必须决定哪一种最好），然后让LLM来导航解决。</p>
<h2 id="bottom-line-llms-makes-powerful-tools-more-accessible">底线：LLM使强大工具更易获取</h2>
<p>底线是，实现面向理念的编程的并不是任何根本性<em>新</em>的东西。但以前要以这种方式工作，你必须是大公司的首席工程师。那样，你才能让初级工程师去苦读文档、处理错误信息。现在，各种条件都不同了，那种工作方式变得更加触手可及。</p>
<p>当然，这引发了一些问题。首席工程师之所以成为首席工程师，部分原因是他们拥有丰富的经验可以借鉴。年轻的工程师能做到同样的工作吗？我认为可以，但这需要时间来找到最佳方式来教授那种判断力。以前这是不可能的，因为工具不到位。</p>
<p>同样真实的是，这种工作方式意味着你花在那种编写代码、将各个部分拼接在一起的"心流状态"的时间更少了。有些人说这让编码变得"无聊"。我并不这样认为。我发现，通过与Claude一起进行头脑风暴和设计，撰写我的计划和RFC，我可以有非常相似——甚至可能更好——的体验。这种构思过程中的许多繁琐工作被移除了，因为Claude可以写出细节，我可以专注于大的部分如何组合在一起。但这也将是我们未来需要更多探索的领域。</p>
<div>
<hr>
<ol>
<li id="fn:1">
<p>亚马逊正在迁移到M365，但目前我仍然通过一个相当古老的Exchange服务器接收邮件。如果一天内邮件至少能刷新一次，我就算过得很开心了，通常它就是卡住不动。&nbsp;<a href="#fnref:1" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:2">
<p>我的家人与《我盛大的希腊婚礼》中的家庭颇为相似。有许多我自认为非常亲近的亲戚，却基本上不知道我们<em>实际上</em>是如何关联的（嗯，直到我搭建了家谱应用才弄清楚）。&nbsp;<a href="#fnref:2" rel="noopener noreferrer">↩︎</a></p>
</li>
</ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 5770 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2025/07/31/rs-py-ts-trifecta/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2025/07/31/rs-py-ts-trifecta/</guid>
      <pubDate>Thu, 31 Jul 2025 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>研讨会：探索新的AI工作流程</title>
      <description>[AI 摘要] 本文介绍了开源项目Symposium，它通过整合MCP服务器、IDE扩展和桌面应用来创建新的AI辅助开发工作流，并邀请开发者参与贡献。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 本文介绍了开源项目Symposium，它通过整合MCP服务器、IDE扩展和桌面应用来创建新的AI辅助开发工作流，并邀请开发者参与贡献。</div><div>
<img src="https://smallcultfollowing.com/babysteps/
/assets/2025-09-24-symposium/logo-alcove.png" alt="Symposium 应用截图" width="25%">
<p>这篇博文将带你了解<a href="https://github.com/symposium-dev/symposium" rel="noopener noreferrer">Symposium</a>，这是一个我在过去一个月左右为之着迷的天马行空的项目。Symposium结合了一个MCP服务器、一个VSCode扩展、一个OS X桌面应用以及一些<a href="https://github.com/symposium-dev/symposium/blob/main/symposium/mcp-server/src/guidance/main.md" rel="noopener noreferrer">引导提示</a>，旨在打造与智能命令行工具协作的新方式。</p>
</div>
<p>Symposium目前主要针对我的设置进行优化，这意味着它最适合与VSCode、Claude、Mac OS X和Rust配合使用。但它的设计理念是保持开放性，因此应该很容易扩展到其他环境（特别是它已经能很好地与其他编程语言协作）。其目标不是与那些工具竞争或取代它们，而是将它们组合在一起，创造出更新更好的东西。</p>
<p>除了介绍Symposium，这篇博文也是一份邀请：<a href="https://github.com/symposium-dev/symposium" rel="noopener noreferrer">Symposium是一个开源项目</a>，我正在寻找愿意一起探索的人！如果你对创造新的AI协作方式感到兴奋，请加入<a href="https://symposium-dev.zulipchat.com" rel="noopener noreferrer">symposium-dev Zulip讨论组</a>。让我们来聊聊吧！</p>
<h2 id="demo-video">演示视频</h2>
<p>我通常不是那种喜欢在网上看视频的人。但在这种特定情况下，我确实认为一部视频抵得上百万字。因此，我在下面嵌入了一段简短的（6分钟）演示Symposium如何工作的视频。来看看吧！不过别担心，如果你不喜欢视频，你也可以直接阅读博文的其他部分。</p>
<div>
      
    </div>

<p>或者，如果你<em>确实</em>喜欢视频，你可以观看<a href="https://youtu.be/HQcIp-IBj0Q" rel="noopener noreferrer">我制作的第一个更详细的版本</a>。那个版本时长20分钟，我后来觉得……有点长了。😁</p>
<h2 id="taskspaces-let-you-juggle-concurrent-agents">任务空间让你同时处理多个智能体</h2>
<p>Symposium的故事始于<code>Symposium.app</code>，一个用于管理<em>任务空间</em>的OS X桌面应用程序。一个任务空间是你的项目副本<sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup>，与一个被指派完成特定任务的智能命令行工具配对。</p>
<p>我的观察是，大多数进行AI开发的人在智能体执行任务时花费大量时间等待。任务空间让你可以快速切换。</p>
<p>在使用任务空间之前，我是通过在不同项目之间跳转来做到的。我发现这非常伤脑筋，因为要不断切换上下文。但在同一项目的<em>任务</em>之间跳转要容易得多。我发现将一个复杂主题与一些简单的重构任务配对效果最好。</p>
<p>以下是使用Symposium的样子：</p>
<img src="https://smallcultfollowing.com/babysteps/
/assets/2025-09-24-symposium/taskspaces.png" alt="Symposium 应用截图" width="100%">
<p>每个方框都是一个任务空间。它在磁盘上拥有自己独立的目录，并关联一个VSCode窗口。当你点击某个任务空间时，应用会将该窗口带到前台。它还可以通过将其他窗口精确地堆叠在第一个窗口后面来隐藏它们<sup id="fnref:2"><a href="#fn:2" rel="noopener noreferrer">2</a></sup>。所以它有点像一个小型窗口管理器。</p>
<p>在每个VSCode窗口中，都有一个终端运行着带有Symposium <a href="https://modelcontextprotocol.io/docs/getting-started/intro" rel="noopener noreferrer">MCP服务器</a>的智能命令行工具。如果你不熟悉MCP，它是一种让大语言模型调用自定义工具的方式；它基本上就是给智能体提供可用工具列表以及它们所需参数的JSON模式。</p>
<p>Symposium的MCP服务器做了很多事情——我们稍后会谈到更多——但其中之一是让智能体与任务空间交互。智能体可以使用MCP服务器发布日志和指示进度（你可以在截图中看到日志）；它还可以生成新的任务空间。我发现后一点非常方便。</p>
<p>我经常在研究一个想法时，发现进行清理或重构的机会。现在我只需快速描述一下要做的工作，生成一个任务空间。下次我无聊时，就可以切换过去开始处理。</p>
<h2 id="an-aside-the-symposium-app-is-written-in-swift-a-language-i-did-not-know-3-weeks-ago">题外话：Symposium应用是用Swift写的，一种我三周前还不懂的语言</h2>
<p>可能值得一提的是，Symposium应用是用Swift编写的。三周前我还不懂Swift。但我现在大约写了6000行代码，并且还在增加。我觉得我已经相当好地掌握了它的工作原理。<sup id="fnref:3"><a href="#fn:3" rel="noopener noreferrer">3</a></sup></p>
<p>嗯，更准确地说，我<em>审查</em>了大约6000行代码，因为大部分时间是Claude生成代码。我主要是阅读代码并提出改进建议<sup id="fnref:4"><a href="#fn:4" rel="noopener noreferrer">4</a></sup>。当我亲自深入编辑代码时，很有趣的是，我发现我对语法没有肌肉记忆。我认为这是智能体工具帮助你快速上手一门新编程语言的很好证据。</p>
<h2 id="walkthroughs-let-ais-explain-code-to-you">引导讲解让AI向你解释代码</h2>
<p>因此，虽然任务空间让你在任务之间跳转，但Symposium的其余部分致力于帮助你完成单个任务。其中很大一部分是尝试超越命令行界面的限制，将智能体连接到IDE。例如，Symposium的MCP服务器有一个名为<code>present_walkthrough</code>的工具，让智能体向你展示解释某些代码如何工作的Markdown文档。这些引导讲解会显示在VSCode的侧边栏中：</p>
<img src="https://smallcultfollowing.com/babysteps/
/assets/2025-09-24-symposium/walkthrough.png" alt="引导讲解截图" width="100%">
<p>如你所见，引导讲解可以嵌入mermaid图表，这非常酷。看到流程图或序列图有时确实能让人豁然开朗。</p>
<p>引导讲解还可以嵌入<em>评论</em>，这些评论锚定在代码的特定部分。你也可以在截图中看到一个，在右侧。</p>
<p>每个评论都有一个回复按钮，让你可以进一步提问或提出修改建议；你还可以选择任意文本并使用名为“在Symposium中讨论”的“代码操作”。这两种操作都会将你带回到运行智能体的终端。它们嵌入了一小段XML（<code>&lt;symposium-ref id="..."/&gt;</code>），然后你就可以像平常一样打字。智能体随后可以使用另一个MCP工具来展开这个引用，以确定你指的是什么或回复的是什么。</p>
<p>在某种程度上，这种“引用我选择的内容”功能是“基本要求”，因为Claude Code已经做到了。但Symposium的版本在任何地方都有效（例如，Q CLI就没有这个功能），更重要的是，它允许同时嵌入多个引用。我发现这非常有用。有时我发出的消息会回复一个评论，同时引用另外两三个东西，而<code>&lt;symposium-ref/&amp;&gt;</code>系统让我可以毫无问题地做到这一点。</p>
<h2 id="integrating-with-ide-knowledge">与IDE知识集成</h2>
<p>Symposium还包括一个<code>ide-operations</code>工具，让智能体可以连接到IDE执行诸如“查找定义”或“查找引用”之类的操作。老实说，我没有注意到这有多重要（Claude非常擅长使用awk/sed），但我也还没有深入研究它。我知道外面还有其他的MCP服务器，比如<a href="https://github.com/oraios/serena" rel="noopener noreferrer">Serena</a>，所以也许正确的做法就是直接导入其中一个，但我觉得通过更深入地集成代码知识，我们<em>可以</em>在这里做很多有趣的事情，所以我一直试图将其“内部化”。</p>
<h2 id="leveraging-rust-conventions">利用Rust惯例</h2>
<p>让我们沿着技术栈继续探索，再看一项功能，即旨在帮助智能体更好地处理Rust代码的MCP工具。到目前为止，其中最有效的一个是我称之为<a href="https://symposium-dev.github.io/symposium/design/mcp-tools/rust-development.html#get_rust_crate_source" rel="noopener noreferrer"><code>get_rust_crate_source</code></a>的工具。它非常简单：给定一个crate的名称，它只是将代码检出到一个临时目录供智能体使用。嗯，实际上，它做的<em>比这</em>多一点。如果智能体提供了一个搜索字符串，它还会搜索该字符串，以便为智能体“快速”找到相关代码，并且它特别强调了examples目录中的代码。</p>
<h2 id="we-could-do-a-lot-more-with-rust">我们可以在Rust方面做更多……</h2>
<p>我的经验是，这个工具使一切变得不同。没有它，Claude只会生成看似合理但实际不存在的API。有了它，Claude通常能精确地知道该做什么。但这真的只是表面功夫，我们能做的远不止于此。既然Symposium的基本结构已经到位，我现在很期待能在这里深入挖掘——例如，我很想开发特定于Rust的代码审查器，可以批评智能体的代码或为其提供架构建议<sup id="fnref:5"><a href="#fn:5" rel="noopener noreferrer">5</a></sup>，或者像<a href="https://github.com/plasma-umass/CWhy" rel="noopener noreferrer">CWhy</a>这样的工具来帮助人们解决Rust的trait错误或宏问题。</p>
<h2 id="and-can-we-decentralize-it">……我们能让它去中心化吗？</h2>
<p>但老实说，我<em>最</em>兴奋的想法是<strong>去中心化</strong>。我希望Rust库作者有一种标准方式来附加自定义指导和说明，以帮助智能体使用他们的库。我希望有一个AI增强版的<code>cargo upgrade</code>，能自动跨越主要版本，利用crate提供的关于变更内容和所需重写的元数据。天啊，我希望库能够附带用WASM实现的MCP服务器（有人听说过<a href="https://opensource.microsoft.com/blog/2025/08/06/introducing-wassette-webassembly-based-tools-for-ai-agents/" rel="noopener noreferrer">Wassette</a>吗？），这样使用该库的Rust开发者就能获得用于处理它的自定义命令和工具。我不完全知道这会是什么样子，但我渴望去探索。如果说我从Rust中学到了一件事，那就是永远要相信生态系统。</p>
<h2 id="looking-further-afield-can-we-use-agents-to-help-humans-collaborate-better">更广泛地看，我们能用智能体帮助人类更好地协作吗？</h2>
<p>我非常好奇要探索的一件事是我们如何利用智能体来帮助人类更好地协作。人们常观察到，与智能体一起编码可能有点孤独<sup id="fnref:6"><a href="#fn:6" rel="noopener noreferrer">6</a></sup>。但我也注意到，为AI消费而构建项目结构需要相对完善的文档。例如，我最近为Symposium做的一件事是创建了“对话请求”流程——一个简化版的Rust RFC流程。我的动机部分是预见性地尝试培养贡献者社区，但也因为几乎所有我做的重大重构或功能开发都始于迭代文档。文档成为了一个集中的跟踪记录，这样我就能清空上下文，并放心地可以继续中断的工作。但一个很好的副作用是，考虑到这一点，项目的文档比预期的要多，我希望这将让人们更容易深入其中并熟悉项目。</p>
<p>那其他事情呢？比如，我认为任务空间真的应该与GitHub issues关联起来。如果我们这样做，我们是否能更好地帮助新贡献者处理issue？或者提供入门的指导说明？</p>
<p>关于记忆呢？我真的想添加某种自动化的记忆系统，以更自动地积累关于系统的知识。但然后我们能否共享这些知识（或其子集），这样当我去修改一个项目时，我就能利用其他一直在处理这个项目的人积累的观察结果来“启动”？</p>
<p>智能体能否帮助引导和管理设计讨论？在工作中，当我分发一份文档时，我通常会下载一份带有人们嵌入评论的文档副本。然后我使用pandoc将其转换为带有HTML注释的Markdown，然后让Claude阅读它并帮助我系统地处理这些评论。我们能否做类似的事情来管理庞杂的RFC讨论线程？</p>
<p>这部分是让我对AI感到兴奋的原因。我的意思是，别误会，我也有点害怕。毫无疑问，AI的普及将改变我们社会的许多事情，而且肯定不总是往好的方面改变。但同时，它也是一个巨大的机会。AI赋予人力量！突然间，学习新东西变得<em>轻松</em>得多。当你想到将AI融入社区流程的潜力时，我认为它完全有可能将我们拉得更近，甚至可能解决开源社区中之前棘手的问题<sup id="fnref:7"><a href="#fn:7" rel="noopener noreferrer">7</a></sup>。</p>
<h2 id="conclusion-want-to-build-something-cool">结论：想构建点酷的东西吗？</h2>
<p>正如我开头所说，这篇博文有两层含义。首先，它是Symposium的一个广告。如果你觉得我描述的东西很酷，那就试试Symposium吧！你可以在这里找到<a href="https://symposium-dev.github.io/symposium/install.html" rel="noopener noreferrer">安装说明</a>。我得提醒你，在撰写本文时，我想我是唯一的用户，所以如果发现安装脚本等里面有bug，我一点也不会感到惊讶。但是嘿，试试看，发现bug并告诉我！或者更好的是，自己修复它们！</p>
<p>但其次，也是更重要的，这篇博文是一份邀请，邀请你出来一起玩<sup id="fnref:8"><a href="#fn:8" rel="noopener noreferrer">8</a></sup>。我渴望有更多人来为Symposium做贡献。我们可以做很多事情！我已经识别出一些“适合首次贡献”的bug。或者，如果你有兴趣承担更大的项目，我有一些受邀的“对话请求”项目，你可以接手并使其成为你自己的。如果这些都不合你意，请随时提出你自己的项目——只需加入<a href="https://symposium-dev.zulipchat.com" rel="noopener noreferrer">Zulip</a>并发起一个话题即可！</p>
<div>
<hr>
<ol>
<li id="fn:1">
<p>技术上是一个git工作树。&nbsp;<a href="#fnref:1" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:2">
<p>这就是“堆叠”框的功能；如果你取消勾选，窗口可以按你喜欢的方式定位。我也正在开发一个平铺布局模式。&nbsp;<a href="#fnref:2" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:3">
<p>嗯，主要是。我仍然有一些关于某个东西不是线程安全的警告，我一直忽略了。Claude向我保证这没什么大不了的（天啊，Claude有时真懒）。&nbsp;<a href="#fnref:3" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:4">
<p>主要就是：“Claude，看在上帝的份上，求你别把每个函数都复制十遍了。”&nbsp;<a href="#fnref:4" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:5">
<p>例如，别傻乎乎地用tokio互斥锁，<a href="https://ryhl.io/blog/actors-with-tokio/" rel="noopener noreferrer">用actor模式</a>。这是我给过的不止一次的具体建议。&nbsp;<a href="#fnref:5" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:6">
<p>我有点不好意思承认，Claude讲的冷笑话偶尔还真能把我逗笑。&nbsp;<a href="#fnref:6" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:7">
<p>旁白音：<em>维护者倦怠。他指的是维护者倦怠。</em>&nbsp;<a href="#fnref:7" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:8">
<p>告诉我你在90年代上过高中，但别直接告诉我。&nbsp;<a href="#fnref:8" rel="noopener noreferrer">↩︎</a></p>
</li>
</ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 9666 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2025/09/24/symposium/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2025/09/24/symposium/</guid>
      <pubDate>Wed, 24 Sep 2025 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Handle trait</title>
      <category>ergonomic-rc</category>
      <description>[AI 摘要] 文章提议在 Rust 中引入名为 Handle 的 trait，用于表示共享资源的句柄，以改进引用计数的人性化设计。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 文章提议在 Rust 中引入名为 Handle 的 trait，用于表示共享资源的句柄，以改进引用计数的人性化设计。</div><p>最近关于符合人体工程学的引用计数有很多讨论。我们举行了一次语言团队设计会议，随后在 RustConf Unconference 上进行了一次影响颇大的讨论。几周来我一直在撰写后续文章，但今天我意识到了本该显而易见的一点——如果我花了这么长时间来写一篇文章，那就意味着这篇文章太长了。所以我将通过一系列更短的文章，专注于个别要点和想法来逐步展开讨论。在第一篇中，我想（a）重新梳理一下背景（b）讨论一个有趣的问题，<strong>我们应该如何称呼这个 trait</strong>。正如标题所建议的，我的提议是 <code>Handle</code>——但我有点超前了。</p>
<h2 id="the-story-thus-far">目前的来龙去脉</h2>
<p>对于那些一直在关注的人来说，关于如何最好地实现符合人体工程学的引用计数，有一个持续的讨论：</p>
<ul>
<li>它始于 2024 年下半年的第一个 Rust 项目目标计划，当时 Dioxus 的 Jonathan Kelley 写了一篇<a href="https://dioxus.notion.site/Dioxus-Labs-High-level-Rust-5fe1f1c9c8334815ad488410d948f05e" rel="noopener noreferrer">关于通向高级 Rust 之路的深思熟虑的博客文章</a>，这最终成为了一个 <a href="https://rust-lang.github.io/rust-project-goals/2024h2/ergonomic-rc.html" rel="noopener noreferrer">2024 年下半年关于符合人体工程学的引用计数的项目目标</a>。</li>
<li>我写了一个<a href="https://smallcultfollowing.com/babysteps/series/claim/" rel="noopener noreferrer">关于我称之为 <code>Claim</code> 的 trait 的系列博客文章</a>。</li>
<li>我和 Josh 谈了谈，Josh 提出了 <a href="https://github.com/rust-lang/rfcs/pull/3680" rel="noopener noreferrer">RFC #3680</a>，该提案提议了一个 <code>use</code> 关键字和 <code>use ||</code> 闭包。我认为，反响是褒贬不一的；是的，这解决了一个实际问题，但对这种方法存在许多担忧。<a href="https://github.com/rust-lang/rfcs/pull/3680#issuecomment-2625526944" rel="noopener noreferrer">我在这里总结了关键点</a>。</li>
<li>Santiago 作为<a href="https://rust-lang.github.io/rust-project-goals/2025h1/ergonomic-rc.html" rel="noopener noreferrer">2025 年上半年项目目标</a>的一部分，实现了对（一个变体的）<a href="https://github.com/rust-lang/rfcs/pull/3680" rel="noopener noreferrer">RFC #3680</a> 的实验性支持。</li>
<li>我撰写了一个 <a href="https://rust-lang.github.io/rust-project-goals/2025h2/ergonomic-rc.html" rel="noopener noreferrer">2025 年下半年项目目标，提议我们创建一个专注于更高级用例的替代 RFC</a>，这促使 Josh 和我进行了一次漫长而富有成效的对话，他说服我这并非正确的方向。</li>
<li>我们在 2025-08-27 举行了一次语言团队设计会议，我在会上展示了这个<a href="https://hackmd.io/@rust-lang-team/B12TpGhKle" rel="noopener noreferrer">关于迄今为止工作的调查和总结</a>。</li>
<li>然后在 <a href="https://2025.rustweek.org/unconf/" rel="noopener noreferrer">RustConf 2025 Unconference</a> 上，我们就这个主题进行了一次大型团体讨论，我认为非常有成效，还有与不同小组的后续对话。</li>
</ul>
<h2 id="this-blog-post-is-about-the-trait">这篇博文是关于“那个 trait”的</h2>
<p>这篇博文的重点在一个特定的问题上：我们应该如何称呼“那个 trait”。在几乎每一个设计中，都存在<em>某种</em> trait，它旨在标识<em>某种东西</em>。但要弄清楚这个<em>某种东西</em>到底是什么<sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup>却很困难。这个 trait 的用途是什么，哪些类型应该实现它？有些事情是明确的：无论那个 trait 是什么，例如 <code>Rc&lt;T&gt;</code> 和 <code>Arc&lt;T&gt;</code> 应该实现它，但仅此而已。</p>
<p>我最初的提议是一个名为 <a href="https://smallcultfollowing.com/babysteps/blog/2024/06/21/claim-auto-and-otherwise/" rel="noopener noreferrer"><code>Claim</code></a> 的 trait，旨在传达“轻量级克隆”——但实际上这个 trait 是<a href="https://smallcultfollowing.com/babysteps/blog/2024/06/26/claim-followup-1/#what-i-really-proposed" rel="noopener noreferrer">旨在取代 <code>Copy</code>，作为哪些克隆应该是显式的定义</a><sup id="fnref:2"><a href="#fn:2" rel="noopener noreferrer">2</a></sup>。Jonathan Kelley 有一个类似的提议，但称之为 <code>Capture</code>。在 <a href="https://github.com/rust-lang/rfcs/pull/3680" rel="noopener noreferrer">RFC #3680</a> 中，提议将该 trait 称为 <code>Use</code>。</p>
<p>细节和意图各不相同，但所有这些尝试都有一个共同点：它们非常<em>操作性</em>。也就是说，这个 trait 总是用它<em>做什么</em>（或不做什么）来定义，而不是它<em>为什么</em>这么做。我认为这永远是像这样的 trait 的一个薄弱基础，容易引起混淆和不同的解释。例如，什么是“轻量级”克隆？它是 O(1) 吗？但那些概率极高是 O(1) 的情况呢？当然，O(1) 并不意味着<em>廉价</em>——它可能每次调用都复制 22GB 的数据。那也是 O(1)。</p>
<p>你需要的是一个相当清楚何时应该以及何时不应该实现的 trait，而不是基于品味或主观标准。<code>Claim</code> 及其同类没有达到这个标准：在 Unconference 上，几位新的 Rust 用户发言说，根据我的解释，他们很难判断自己的类型是否应该实现那个 trait（无论我们叫它什么）。这在 RFC 和其他地方也是一个持续的主题。</p>
<h2 id="shouldnt-we-call-it-share-hat-tip-jack-huey">“我们不应该叫它 <em>share</em> 吗？”（致谢：Jack Huey）</h2>
<p>但实际上这里确实有一个语义基础，Jack Huey 首先提出了这一点。考虑这个问题。克隆 <code>Mutex&lt;Vec&lt;u32&gt;&gt;</code> 和 <code>Arc&lt;Mutex&lt;Vec&lt;u32&gt;&gt;&gt;</code> 有什么区别？</p>
<p>一个区别当然是成本。克隆 <code>Mutex&lt;Vec&lt;u32&gt;&gt;</code> 会深克隆向量，克隆 <code>Arc</code> 只会增加引用计数。</p>
<p>但更重要的区别是我所说的<em>“纠缠”</em>。当你克隆 <code>Arc</code> 时，你不会得到一个新的值——你得到的是一个<em>指向相同值的第二个句柄</em>。<sup id="fnref:3"><a href="#fn:3" rel="noopener noreferrer">3</a></sup></p>
<h2 id="entanglement-changes-the-meaning-of-the-program">纠缠改变了程序的含义</h2>
<p>知道哪些值是“纠缠的”是理解程序行为的关键。借用检查器<sup id="fnref:4"><a href="#fn:4" rel="noopener noreferrer">4</a></sup> 实现可靠性的很大一部分是通过减少“纠缠”来实现的，因为在 Rust 中处理它相对麻烦。</p>
<p>考虑以下代码。<code>l_before</code> 和 <code>l_after</code> 的值会是什么？</p>
<div><pre><code><span><span><span>let</span><span> </span><span>l_before</span><span> </span><span>=</span><span> </span><span>v1</span><span>.</span><span>len</span><span>();</span><span>
</span></span></span><span><span><span>let</span><span> </span><span>v2</span><span> </span><span>=</span><span> </span><span>v1</span><span>.</span><span>clone</span><span>();</span><span>
</span></span></span><span><span><span>v2</span><span>.</span><span>push</span><span>(</span><span>new_value</span><span>);</span><span>
</span></span></span><span><span><span>let</span><span> </span><span>l_after</span><span> </span><span>=</span><span> </span><span>v1</span><span>.</span><span>len</span><span>();</span><span>
</span></span></span></code></pre></div><p>答案当然是“取决于 <code>v1</code> 的类型”。如果 <code>v1</code> 是一个 <code>Vec</code>，那么 <code>l_after == l_before</code>。但如果 <code>v1</code> 是，比如说，像这样的结构体：</p>
<div><pre><code><span><span><span>struct</span> <span>SharedVec</span><span>&lt;</span><span>T</span><span>&gt;</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>data</span>: <span>Arc</span><span>&lt;</span><span>Mutex</span><span>&lt;</span><span>Vec</span><span>&lt;</span><span>T</span><span>&gt;&gt;&gt;</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>impl</span><span>&lt;</span><span>T</span><span>&gt;</span><span> </span><span>SharedVec</span><span>&lt;</span><span>T</span><span>&gt;</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>pub</span><span> </span><span>fn</span> <span>push</span><span>(</span><span>&amp;</span><span>self</span><span>,</span><span> </span><span>value</span>: <span>T</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>self</span><span>.</span><span>data</span><span>.</span><span>lock</span><span>().</span><span>unwrap</span><span>().</span><span>push</span><span>(</span><span>value</span><span>);</span><span>
</span></span></span><span><span><span>    </span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>    </span><span>pub</span><span> </span><span>fn</span> <span>len</span><span>(</span><span>&amp;</span><span>self</span><span>)</span><span> </span>-&gt; <span>usize</span> <span>{</span><span>
</span></span></span><span><span><span>        </span><span>self</span><span>.</span><span>data</span><span>.</span><span>lock</span><span>().</span><span>unwrap</span><span>().</span><span>len</span><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></pre></div><p>那么 <code>l_after == l_before + 1</code>。</p>
<p>有许多类型的行为类似于 <code>SharedVec</code>：<code>Rc</code> 和 <code>Arc</code> 当然如此，但像 <a href="https://docs.rs/bytes/latest/bytes/struct.Bytes.html" rel="noopener noreferrer"><code>Bytes</code></a> 和通道端点（如 <a href="https://doc.rust-lang.org/std/sync/mpsc/struct.Sender.html" rel="noopener noreferrer"><code>Sender</code></a>）这样的东西也是如此。所有这些都是指向底层值的“句柄”的示例，当你克隆它们时，你会得到第二个与第一个无法区分的句柄。</p>
<h2 id="we-have-a-name-for-this-concept-already-handles">我们已经有一个这个概念的名字了：句柄</h2>
<p>Jack 的见解是，我们应该关注<em>语义概念</em>（共享），而不是操作细节（如何实现）。这清楚地表明了何时应该实现这个 trait。我非常喜欢这个想法，尽管我最终决定不喜欢 <code>Share</code> 这个名字。我觉得这个词不够具体，用户可能没有意识到它指的是一个特定的概念：“可共享类型”听起来确实不太对。但实际上，这个概念已经有一个常用名称：句柄（参见例如 <a href="https://docs.rs/tokio/latest/tokio/runtime/struct.Handle.html" rel="noopener noreferrer"><code>tokio::runtime::Handle</code></a>）。</p>
<p>这就是我如何得出我对那个 trait 的提议名称和定义，即 <code>Handle</code>：<sup id="fnref:5"><a href="#fn:5" rel="noopener noreferrer">5</a></sup></p>
<div><pre><code><span><span><span>/// Indicates that this type is a *handle* to some
</span></span></span><span><span><span>/// underlying resource. The `handle` method is
</span></span></span><span><span><span>/// used to get a fresh handle.
</span></span></span><span><span><span>trait</span><span> </span><span>Handle</span>: <span>Clone</span> <span>{</span><span>
</span></span></span><span><span><span>    </span><span>final</span><span> </span><span>fn</span> <span>handle</span><span>(</span><span>&amp;</span><span>self</span><span>)</span><span> </span>-&gt; <span>Self</span> <span>{</span><span>
</span></span></span><span><span><span>        </span><span>Clone</span>::<span>clone</span><span>(</span><span>self</span><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></pre></div><h2 id="we-would-lint-and-advice-people-to-call-handle">我们会对调用 <code>clone</code> 进行 lint 并建议人们调用 <code>handle</code></h2>
<p><code>Handle</code> trait 包含一个 <code>handle</code> 方法，它<em>始终</em>等同于 <code>clone</code>。这个方法的目的是向读者表明，结果是指向相同底层值的第二个句柄。</p>
<p>一旦 <code>Handle</code> trait 存在，当接收者已知实现了 <code>Handle</code> 时，我们应该对调用 <code>clone</code> 进行 lint，并鼓励人们改为调用 <code>handle</code>：</p>
<div><pre><code><span><span><span>impl</span><span> </span><span>DataStore</span> <span>{</span><span>
</span></span></span><span><span><span>    </span><span>fn</span> <span>store_map</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>,</span><span> </span><span>map</span>: <span>&amp;</span><span>Arc</span><span>&lt;</span><span>HashMap</span><span>&lt;..</span><span>.</span><span>&gt;&gt;</span><span>)</span> <span>{</span><span>
</span></span></span><span><span><span>        </span><span>self</span><span>.</span><span>stored_map</span> <span>=</span><span> </span><span>map</span><span>.</span><span>clone</span><span>();</span><span>
</span></span></span><span><span><span>        </span><span>//                    -----
</span></span></span><span><span><span>        </span><span>//
</span></span></span><span><span><span>        </span><span>// Lint: convert `clone` to `handle` for
</span></span></span><span><span><span>        </span><span>// greater clarity.
</span></span></span><span><span><span>    </span><span>}</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>将上面的代码与 lint 建议的使用 <code>handle</code> 的版本进行比较，我认为你会理解 <code>handle</code> 如何提高正在发生的事情的清晰度：</p>
<div><pre><code><span><span><span>impl</span><span> </span><span>DataStore</span> <span>{</span><span>
</span></span></span><span><span><span>    </span><span>fn</span> <span>store_map</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>,</span><span> </span><span>map</span>: <span>&amp;</span><span>Arc</span><span>&lt;</span><span>HashMap</span><span>&lt;..</span><span>.</span><span>&gt;&gt;</span><span>)</span> <span>{</span><span>
</span></span></span><span><span><span>        </span><span>self</span><span>.</span><span>stored_map</span> <span>=</span><span> </span><span>map</span><span>.</span><span>handle</span><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></pre></div><h2 id="what-it-means-to-be-a-handle">作为<em>句柄</em>意味着什么</h2>
<p><em>句柄</em>的定义特征是，当克隆它时，会产生第二个访问相同底层值的值。这意味着两个句柄是“纠缠的”，对一个句柄的内部变更会体现在另一个句柄上。反映这一点的是，大多数句柄的 API 完全或几乎完全由 <code>&amp;self</code> 方法组成，因为拥有对<em>句柄</em>的独占访问权并不一定意味着你对<em>值</em>有独占访问权。</p>
<p>句柄通常只在涉及内部可变性时才在语义上重要。有两个指向不可变值的句柄并没有<em>错</em>，但它通常与同一值的两个副本无法区分。这使得持久化集合成为一个有趣的灰色地带：我可能会为像 <code>im::Vec&lt;T&gt;</code> 这样的东西实现 <code>Handle</code>，特别是因为像 <code>im::Vec&lt;Cell&lt;u32&gt;&gt;</code> 这样的东西<em>确实</em>会使纠缠可见，但我认为也有理由反对这样做。</p>
<h2 id="handles-in-the-stdlib">标准库中的句柄</h2>
<p>在标准库中，handle 将恰好为一个 <code>Copy</code> 类型实现（其他都是值）：</p>
<div><pre><code><span><span><span>// Shared references, when cloned (or copied),
</span></span></span><span><span><span>// create a second reference:
</span></span></span><span><span><span>impl</span><span>&lt;</span><span>T</span>: <span>?</span><span>Sized</span><span>&gt;</span><span> </span><span>Handle</span> <span>for</span><span> </span><span>&amp;</span><span>T</span> <span>{}</span><span>
</span></span></span></code></pre></div><p>它将为引用计数指针（但不包括 <code>Box</code>）实现：</p>
<div><pre><code><span><span><span>// Ref-counted pointers, when cloned,
</span></span></span><span><span><span>// create a second reference:
</span></span></span><span><span><span>impl</span><span>&lt;</span><span>T</span>: <span>?</span><span>Sized</span><span>&gt;</span><span> </span><span>Handle</span> <span>for</span><span> </span><span>Rc</span><span>&lt;</span><span>T</span><span>&gt;</span> <span>{}</span><span>
</span></span></span><span><span><span>impl</span><span>&lt;</span><span>T</span>: <span>?</span><span>Sized</span><span>&gt;</span><span> </span><span>Handle</span> <span>for</span><span> </span><span>Arc</span><span>&lt;</span><span>T</span><span>&gt;</span> <span>{}</span><span>
</span></span></span></code></pre></div><p>并且它将为像通道端点这样在底层使用引用计数值实现的类型实现：</p>
<div><pre><code><span><span><span>// mpsc "senders", when cloned, create a
</span></span></span><span><span><span>// second sender to the same underlying channel:
</span></span></span><span><span><span>impl</span><span>&lt;</span><span>T</span>: <span>?</span><span>Sized</span><span>&gt;</span><span> </span><span>Handle</span> <span>for</span><span> </span><span>mpsc</span>::<span>Sender</span> <span>{}</span><span>
</span></span></span></code></pre></div><h2 id="conclusion-a-design-axiom-emerges">结论：一个设计公理浮出水面</h2>
<p>好吧，我将就此结束这篇“短小精悍”的博文。还有更多内容要来！但在结束之前，让我阐述一下我认为应该为这个设计采用的一个有用的“设计公理”：</p>
<blockquote>
<p><strong>暴露纠缠。</strong> 理解指向底层值的<em>句柄</em>与值本身之间的区别对于理解 Rust 如何工作至关重要。</p>
</blockquote>
<p>这个措辞感觉有点别扭，但我认为这仍然是关键部分。</p>
<div>
<hr>
<ol>
<li id="fn:1">
<p>那，我的朋友们，就是<em>伏笔</em>。该死我真是太棒了。&nbsp;<a href="#fnref:1" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:2">
<p>我将 <code>Claim</code> 描述为一种“轻量级克隆”，但在 Unconference 上有人指出，“重型拷贝”可能是对我意图的更好描述。&nbsp;<a href="#fnref:2" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:3">
<p>并且，并非巧合的是，克隆会导致纠缠的类型往往也是克隆成本低廉的类型。&nbsp;<a href="#fnref:3" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:4">
<p>以及函数式编程……&nbsp;<a href="#fnref:4" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:5">
<p>“final”关键字由 Josh Triplett 在 RFC 3678 中提出。它意味着实现不能更改 <code>Handle::handle</code> 的定义。关于它是否应该更名或更通用等等，已经有一些反复讨论；我所知道的是，我认为这是一个非常有用的概念，适用于你想要用户能够选择使一个方法<em>可用</em>但<em>不能</em>改变其功能的情况。你可以用其他方式做到这一点，它们只是更奇怪。&nbsp;<a href="#fnref:5" rel="noopener noreferrer">↩︎</a></p>
</li>
</ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 15853 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2025/10/07/the-handle-trait/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2025/10/07/the-handle-trait/</guid>
      <pubDate>Tue, 7 Oct 2025 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>SymmACP：扩展 Zed 的 ACP 以支持可组合智能体</title>
      <description>[AI 摘要] 本文提出了对 Zed 的智能体客户端协议的扩展方案 SymmACP，旨在通过可组合的代理架构，让开发者能够模块化地构建、集成和定制 AI 工具功能。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 本文提出了对 Zed 的智能体客户端协议的扩展方案 SymmACP，旨在通过可组合的代理架构，让开发者能够模块化地构建、集成和定制 AI 工具功能。</div><p>本文介绍了<strong>SymmACP</strong>——这是对 Zed 的<a href="https://agentclientprotocol.com/overview/introduction" rel="noopener noreferrer">智能体客户端协议</a>提出的扩展方案，旨在让你能够像构建 Unix 管道或浏览器扩展一样，构建 AI 工具。想要一个更好的终端用户界面？在 GitHub 上发现了一些很酷的斜杠命令？更喜欢不同的后端？借助 SymmACP，你可以混合搭配这些组件，并让它们协同工作，彼此无需知晓。</p>
<p>这与当今 AI 工具的工作方式大相径庭——当前一切都是单体架构——如果你想改变某个部分，就不得不从头重建整个系统。SymmACP 允许你以分层、可互操作的方式构建新功能和新的交互模式。本文将通过一系列示例来解释 SymmACP 的工作原理。</p>
<p>目前，SymmACP 仍只是一个思想实验。我已经向 Zed 团队勾勒了这些想法，他们似乎很感兴趣，但我们需要就本文讨论的细节进行进一步探讨。我计划在<a href="https://symposium-dev.github.io/symposium/" rel="noopener noreferrer">Symposium</a>中开始原型开发——如果你觉得我在此讨论的想法令人兴奋，请加入 <a href="https://symposium-dev.zulipchat.com/" rel="noopener noreferrer">Symposium Zulip</a> 让我们聊聊！</p>
<h2 id="composable-agents-let-you-build-features-independently-and-then-combine-them">“可组合智能体”让你能独立构建功能，然后再将它们组合起来</h2>
<p>我将通过一系列功能来解释“可组合智能体”的概念。我们首先从一个基础的 CLI 智能体工具<sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup>开始——基本上是一个可以访问某些 MCP 服务器、用于读写文件和执行 bash 命令的聊天循环。然后我们将展示如何在此基础上添加多个功能：</p>
<ol>
<li>解决时间盲区，让智能体知道当前时间。</li>
<li>为智能体注入上下文和“个性”。</li>
<li>生成长时间运行的异步任务。</li>
<li>克隆 Q CLI 的 <code>/tangent</code> 模式，允许你进行一些稍后将从历史记录中移除的“私下”工作。</li>
<li>实现<a href="https://symposium-dev.github.io/symposium/get-started/walkthroughs.html" rel="noopener noreferrer"> Symposium 的交互式引导</a>，为智能体提供比纯文本更丰富的沟通词汇。</li>
<li>更智能的工具委派。</li>
</ol>
<p><strong>神奇之处在于，这些功能中的每一个都将作为独立的代码库开发。</strong>此外，它们可以应用于任何你希望的基础工具，只要该工具支持 SymmACP。你还可以将它们与不同的前端结合使用，例如终端用户界面、Web 前端、<a href="https://zed.dev/" rel="noopener noreferrer">Zed</a> 或 <a href="https://zed.dev/blog/jetbrains-on-acp" rel="noopener noreferrer">IntelliJ</a> 的内置支持等。非常棒。</p>
<p>我的希望是，如果我们能统一采用 SymmACP 或类似的协议，那么我们就能从每个人开发自己定制工具的现状，转变为一个可以相互借鉴、可互操作的创意生态系统。</p>
<h2 id="let-mut-symmacp--acp">let mut SymmACP = ACP</h2>
<p>SymmACP 起始于 ACP，所以让我们先解释一下 ACP 是什么。ACP 是一个极其简单的协议，让你能抽象化 CLI 智能体。想象一下，你正在使用一个智能 CLI 工具，但其通信不是通过终端进行，而是通过 JSON-RPC 消息（目前通过 stdin/stdout 发送）与前端通信。</p>
<pre>flowchart LR
    Editor &lt;-.-&gt;|JSON-RPC via stdin/stdout| Agent[CLI Agent]
  </pre>
<p>当你在图形界面中输入内容时，编辑器会通过 JSON-RPC 消息将你输入的内容发送给智能体。智能体会以包含文本和图像的消息流进行回复。如果智能体决定调用某个工具，它可以通过向编辑器发送 JSON-RPC 消息来请求许可。当智能体完成任务后，它会向编辑器发送一个“结束回合”消息，表示“我现在准备好让你再次输入了”。</p>
<pre>sequenceDiagram
    participant E as Editor
    participant A as Agent
    participant T as Tool (MCP)
    
    E-&gt;&gt;A: prompt("Help me debug this code")
    A-&gt;&gt;E: request_permission("Read file main.rs")
    E-&gt;&gt;A: permission_granted
    A-&gt;&gt;T: read_file("main.rs")
    T-&gt;&gt;A: file_contents
    A-&gt;&gt;E: text_chunk("I can see the issue...")
    A-&gt;&gt;E: text_chunk("The problem is on line 42...")
    A-&gt;&gt;E: end_turn
  </pre>
<h2 id="telling-the-agent-what-time-it-is">告诉智能体当前时间</h2>
<p>好，让我们来实现我们的第一个功能。如果你用过 CLI 智能体，你可能已经注意到它们不知道当前时间——甚至不知道当前是<em>哪一年</em>。这听起来可能微不足道，但可能导致一些真实的错误。例如，它们可能没有意识到某些信息已经过时。或者，当它们进行网络搜索时，可能会搜索错误的内容：我见过 CLI 智能体在网上搜索“2024 年的 API 更新”，即使当时是 2025 年。</p>
<p>为了解决这个问题，许多 CLI 智能体会在你的提示词中注入一些额外的文本，类似于 <code>&lt;current-date date="2025-10-08" time="HH:MM:SS"/&gt;</code>。这为 LLM 提供了所需的上下文。</p>
<p>那么，如何使用 ACP 来实现这个功能呢？想法是创建一个<strong>代理</strong>。这个代理将包装原始的 ACP 服务器：</p>
<pre>flowchart LR
    Editor[Editor/VSCode] &lt;--&gt;|ACP| Proxy[Datetime Proxy] &lt;--&gt;|ACP| Agent[CLI Agent]
  </pre>
<p>这个代理将为收到的每条“提示”消息添加日期和时间信息：</p>
<pre>sequenceDiagram
    participant E as Editor
    participant P as Proxy
    participant A as Agent
    
    E-&gt;&gt;P: prompt("What day is it?")
    P-&gt;&gt;A: prompt(" What day is it?")
    A-&gt;&gt;P: text_chunk("It is 2025-10-08.")
    P-&gt;&gt;E: text_chunk("It is 2025-10-08.")
    A-&gt;&gt;P: end_turn
    P-&gt;&gt;E: end_turn
  </pre>
<p>很简单，对吧？当然，这可以与任何编辑器和任何支持 ACP 的工具一起使用。</p>
<h2 id="next-feature-injecting-personality-to-the-agent">下一个功能：为智能体注入“个性”</h2>
<p>让我们来看另一个基本“源自” ACP 的功能：注入个性。大多数智能体都提供了以各种方式配置“上下文”的能力——或者 Claude Code 所称的<a href="https://docs.claude.com/en/docs/claude-code/memory" rel="noopener noreferrer">记忆</a>。这很有用，但我和其他人已经注意到，如果你想改变 Claude 的“行为方式”——即让它更具协作性——这还不够。你真的需要在对话开始时就强化这种模式。</p>
<p>在 Symposium 中，<a href="https://github.com/symposium-dev/symposium/blob/7f437fdf02ab52cd0bd3070d25feaad387b6d23f/symposium/mcp-server/src/server.rs#L885" rel="noopener noreferrer">“yiasou”提示</a>（对于不说希腊语的朋友，也提供“hi”选项😛）旨在在对话开始时作为第一个操作执行。但是，MCP 服务器无法<em>确保</em>用户会用 <code>/symposium:hi</code> 或类似命令开启对话。当然，如果 Symposium 作为 ACP 服务器实现，我们绝对可以做到这一点：</p>
<pre>sequenceDiagram
    participant E as Editor
    participant P as Proxy
    participant A as Agent
    
    E-&gt;&gt;P: prompt("I'd like to work on my document")
    P-&gt;&gt;A: prompt("/symposium:hi")
    A-&gt;&gt;P: end_turn
    P-&gt;&gt;A: prompt("I'd like to work on my document")
    A-&gt;&gt;P: text_chunk("Sure! What document is that?") 
    P-&gt;&gt;E: text_chunk("Sure! What document is that?") 
    A-&gt;&gt;P: end_turn
    P-&gt;&gt;E: end_turn
  </pre>
<h2 id="proxies-are-a-better-version-of-hooks">代理是钩子（hooks）的更好版本</h2>
<p>你们中的一些人可能会说：“嗯，这不是<a href="https://docs.claude.com/en/docs/claude-code/hooks" rel="noopener noreferrer">钩子</a>的作用吗？”是的，你可以用钩子实现，但这有两个问题。首先，钩子是非标准的，所以你必须为每个智能体进行不同的处理。</p>
<p>钩子的第二个问题是，它们<strong>从根本上受限于</strong>钩子设计者预期的用途。你只能在工具提供的流程中设置钩子，并且只能控制工具允许你控制的内容。下一个功能开始展示我的意思：据我所知，它无法用我想要的方式轻松通过钩子实现。</p>
<h2 id="next-feature-long-running-asynchronous-tasks">下一个功能：长时间运行的异步任务</h2>
<p>让我们继续下一个功能：长时间运行的异步任务。这个功能将超越 ACP 当前的能力，进入扩展的“SymmACP”功能集。</p>
<p>目前，当服务器调用 MCP 工具时，它是以阻塞方式执行的。但有时它执行的任务可能又长又复杂。你真正想要的是一种“启动”任务然后可以回去继续工作的机制。当任务完成时，你（和智能体）可以收到通知。</p>
<p>这在我进行“深度研究”时经常遇到。我工作流程的一个重要部分是，当我遇到无法理解的问题时，我会派出一个研究智能体去网络上搜集信息。通常，我会让正在协作的智能体准备一个研究提示，总结我们尝试过的内容、遇到的障碍以及其他相关细节。然后我会切换到 <a href="https://claude.ai/" rel="noopener noreferrer">claude.ai</a> 或 <a href="https://gemini.google.com/" rel="noopener noreferrer">Gemini Deep Research</a> 并粘贴这个提示。这通常需要 5-10 分钟，并生成一份 markdown 报告。我会下载这份报告交给我的智能体。这通常能帮助我们解决问题。<sup id="fnref:2"><a href="#fn:2" rel="noopener noreferrer">2</a></sup></p>
<p>这个研究流程效果很好，但很繁琐，需要我复制粘贴。我理想中的情况是有一个 MCP 工具能为我完成搜索，并在结果准备好时立即将结果交给智能体处理。同时，在我们等待的过程中，我希望能继续与智能体一起工作。不幸的是，据我所知，工具协议没有提供此类异步通知的机制。</p>
<h2 id="symmacp--tool-invocations--unprompted-sends">SymmACP += 工具调用 + 无提示发送</h2>
<p>那么，如何用 SymmACP 实现呢？嗯，我希望以两种方式扩展当前的 ACP 协议：</p>
<ol>
<li>我希望 ACP 代理能够提供由<em>代理本身</em>执行的工具。目前，智能体负责执行所有工具；ACP 协议仅在请求<em>许可</em>时起作用。但是，让代理通过 ACP 发送消息来执行 MCP 工具会很容易。</li>
<li>我希望有一种方式让<em>智能体</em>能够主动向<em>编辑器</em>发起响应。目前，编辑器总是通过提示来启动每个通信会话；但在这种情况下，智能体可能希望在没有提示的情况下发送消息。</li>
</ol>
<p>在这种情况下，我们可以这样实现我们的研究代理：</p>
<pre>sequenceDiagram
    participant E as Editor
    participant P as Proxy
    participant A as Agent
    
    E-&gt;&gt;P: prompt("Why is Rust so great?")
    P-&gt;&gt;A: prompt("Why is Rust so great?")
    A-&gt;&gt;P: invoke tool("begin_research")
    activate P
    P-&gt;&gt;A: ok
    A-&gt;&gt;P: "I'm looking into it!"
    P-&gt;&gt;E: "I'm looking into it!"
    A-&gt;&gt;P: end_turn
    P-&gt;&gt;E: end_turn

    Note over E,A: 时间流逝（5-10分钟），用户继续工作...
    Note over P: 研究在后台完成
    
    P-&gt;&gt;A: 
    deactivate P
    A-&gt;&gt;P: "Research says Rust is fast"
    P-&gt;&gt;E: "Research says Rust is fast"
    A-&gt;&gt;P: end_turn
    P-&gt;&gt;E: end_turn
  </pre>
<p>这样做的好处是，代理封装了整个流程：它知道如何进行研究，并在研究完成时管理通知各个参与者。（此外，这还依赖于我之前省略的一个细节，即）</p>
<h2 id="next-feature-tangent-mode">下一个功能：切线模式</h2>
<p>让我们探索下一个功能，<a href="https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-experimental-features.html" rel="noopener noreferrer"> Q CLI 的 <code>/tangent</code> 模式</a>。这个功能很有趣，因为它是一个简单（但有用！）的编辑历史记录的示例。<code>/tangent</code> 的工作方式是，当你首次输入 <code>/tangent</code> 时，Q CLI 会保存当前状态。然后你可以像往常一样继续，但当你<em>再次</em>输入 <code>/tangent</code> 时，你的状态会恢复到之前的状态。正如其名所示，这让你能够探索一个侧面对话，而不会污染你的主上下文。</p>
<p>在 SymmACP 中支持切线模式的基本思路是，代理将（a）拦截切线提示并记住其开始位置；（b）允许对话正常继续；然后（c）当需要结束切线时，创建一个新会话并重播切线开始之前的历史记录<sup id="fnref:3"><a href="#fn:3" rel="noopener noreferrer">3</a></sup>。</p>
<h2 id="symacp--replay">SymACP += 重播</h2>
<p>你可以在当前的 ACP 中实现“切线”功能，但还不完全可行。在 ACP 中，智能体始终拥有会话历史记录的所有权。编辑器可以创建新会话或加载旧会话；加载旧会话时，智能体会“重播”事件以便编辑器重建 GUI。但没有办法让<em>编辑器</em>向<em>智能体</em>“重播”或构建会话。相反，编辑器只能发送提示，这将引起智能体的回复。在这种情况下，我们希望能够说“创建一个新聊天，其中我说过这个，你回应过那个”，这样我们就可以设置初始状态。这样，我们就可以轻松创建一个包含旧会话消息的新会话。</p>
<p>所以，具体工作方式如下：</p>
<pre>sequenceDiagram
    participant E as Editor
    participant P as Proxy
    participant A as Agent
    
    E-&gt;&gt;P: prompt("Hi there!")
    P-&gt;&gt;A: prompt("Hi there!")

    Note over E,A: 对话进行中
    
    E-&gt;&gt;P: prompt("/tangent")
    Note over P: 代理记录对话状态
    P-&gt;&gt;E: end_turn
    E-&gt;&gt;P: prompt("btw, ...")
    P-&gt;&gt;A: prompt("btw, ...")

    Note over E,A: 对话进行中
    
    E-&gt;&gt;P: prompt("/tangent")
    
    P-&gt;&gt;A: new_session
    P-&gt;&gt;A: prompt("Hi there!")    
    Note over P,A: ...代理重播对话...
  </pre>
<h2 id="next-feature-interactive-walkthroughs">下一个功能：交互式引导</h2>
<p>Symposium 的一个很棒的功能是能够进行<a href="https://symposium-dev.github.io/symposium/get-started/walkthroughs.html" rel="noopener noreferrer">交互式引导</a>。这些引导包括一个 HTML 侧边栏以及代码中的内联注释：</p>
<img src="https://smallcultfollowing.com/babysteps/
/assets/2025-symmacp/walkthrough.png" alt="引导截图" width="100%">
<p>目前，这是通过一种有点 hack 的方式实现的：</p>
<ul>
<li>智能体调用一个 MCP 工具并向其发送 markdown 格式的引导内容。这个 markdown 包括旨在放置在特定行的命令，这些行不是通过行号标识（智能体不擅长处理行号），而是通过符号名称或搜索字符串标识。</li>
<li>MCP 工具解析 markdown，确定注释的行号，并生成 HTML。它通过 IPC 将该 HTML 发送到 VSCode 扩展。</li>
<li>VSCode 收到 IPC 消息，在侧边栏中显示 HTML，并在代码中创建注释。</li>
</ul>
<p>它能用，但这是一个巨大的鲁布·戈德堡机械。</p>
<h2 id="symmacp--enriched-conversation-history">SymACP += 丰富的对话历史</h2>
<p>使用 SymmACP，我们会将穿透机制构建为一个代理。就像现在一样，它会向智能体提供一个 MCP 工具来接收引导 markdown。然后，它将把 markdown 转换为 HTML 显示在侧边栏，以及各种需要嵌入代码的注释。但这就是不同之处。</p>
<p>与其通过 IPC 发送这些内容，我更希望让代理能够在聊天中传递额外的信息。在当前的 ACP 中这相对容易做到，因为它提供了各种功能，但我想再进一步</p>
<p>我会有一个管理引导的代理层。正如我们之前所见，它会提供一个工具。但还有一件额外的事情，即除了聊天历史记录之外，它还能够传递额外的状态。我认为基本的对话结构是这样的：</p>
<ul>
<li>对话
<ul>
<li>回合
<ul>
<li>用户提示（零个或多个）</li>
<li>响应（零个或多个）</li>
<li>工具使用（零个或多个）</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>但我认为（a）能够为任何这些内容附加元数据会很有用，例如添加关于<em>对话本身</em>或<em>特定回合</em>（甚至特定<em>提示</em>）的额外上下文，但也要添加其他类型的事件。例如，工具批准是一个<em>事件</em>。而展示引导和添加注释也是一个事件。</p>
<p>按照我的设想，SymmACP 的一个核心功能将是能够将你的状态序列化为 JSON。你将能够要求 SymmACP 参与者总结一个会话。它们会依次要求任何委托方进行总结，并在此过程中添加自己的元数据。你也可以向<em>另一个方向</em>发送请求——例如，智能体可以向编辑器展示其状态，并要求其增强。</p>
<h2 id="enriched-history-would-let-walkthroughs-be-extra-metadata">丰富的历史记录将使引导成为额外的元数据</h2>
<p>这意味着引导代理可以在聊天记录中添加额外的元数据，如“当前引导”和“当前已放置的注释”。然后<em>编辑器</em>要么知道这些元数据，要么不知道。如果不知道，你就不会在聊天中看到它。哦，或者我们可以做一些类似 HTML 的处理，有一种“优雅降级”的方式（例如，引导可以作为普通的“响应”呈现，但带有一些元数据，如果你知道如何查找，它会告诉你以不同方式解释）。但如果编辑器<em>确实</em>知道元数据，它会特别处理，将引导显示在面板中，并将注释添加到代码中。</p>
<p>有了丰富的历史记录，我认为我们甚至可以说，在 SymmACP 中，加载、保存和持久化会话<em>本身</em>就变成了一个扩展，可以由代理实现；基础协议只需要能够执行和序列化对话。</p>
<h2 id="final-feature-smarter-tool-delegation">最后一个功能：更智能的工具委派</h2>
<p>让我勾勒另一个我一直在琢磨的功能，我觉得它会很酷。众所周知，当可用的 MCP 工具太多时，LLM 会感到困惑。它们会分心。这是有道理的，如果我面对一整本电话簿大小的可能操作列表，被要求弄清楚该做什么，我也会这样。我可能根本不会理会。</p>
<p>但人类如何处理这个问题呢？嗯，我们不会整本地拿电话簿——我们会先拿到一个较短的<em>类别</em>列表，然后深入。所以我去文件菜单，<em>然后</em>得到选项列表，而不是一个扁平的命令列表。</p>
<p>我想尝试构建一个类似的 MCP 工具来提供 IDE 功能。现代 IDE 可以“做”的事情多如牛毛。它可以查找引用。它可以查找定义。它可以获取类型提示。它可以重命名。它可以提取方法。事实上，这个列表甚至是开放式的，因为扩展可以提供它们<em>自己的</em>命令。我不知道所有那些功能<em>到底是什么</em>，但我对 IDE 能做的<em>事情类型</em>有一个大致的了解——我怀疑模型也是。</p>
<p>如果你给它们一个单一的工具，“IDE 操作”，并且它们可以用简单的英语描述它们想要什么，会怎么样？例如，<code>ide_operation("查找引用 HTTP 代理的 ProxyHandler 的定义"</code>）。嗯，这听起来很像一个委派者，或一个子智能体。因为现在你需要使用第二个 LLM 来解释这个请求——你可能需要给它一组建议的 IDE 功能列表，并让它找出完整细节，然后让它制定一个计划（或可能直接执行工具）来找到答案。</p>
<p>碰巧的是，MCP <em>确实</em>有能力让工具这样做——它叫做（在我看来有点奇怪的）“采样”。它允许 MCP 工具向 LLM 进行“回调”。但据我所知，实际上<em>没有人</em>实现它。<sup id="fnref:4"><a href="#fn:4" rel="noopener noreferrer">4</a></sup> 但采样本身就有点局限。使用 SymmACP，我认为你可以做更有趣的事情。</p>
<h2 id="symmacpcontainssimultaneous_sessions">SymmACP.contains(并发会话)</h2>
<p>关键在于，ACP 已经允许单个智能体“同时提供”多个会话。所以这意味着如果我有一个代理，也许它提供了一个 MCP 工具定义，我可以使用它来启动<em>全新的</em>会话——结合我上面提到的“历史重播”能力，该工具可以精确控制要带入该会话的起始上下文，这非常酷（这对今天的 MCP 服务器来说是一个挑战，它们无法访问对话历史记录）。</p>
<pre>sequenceDiagram
    participant E as Editor
    participant P as Proxy
    participant A as Agent
    
    A-&gt;&gt;P: ide_operation("...")
    activate P
    P-&gt;&gt;A: new_session
    activate P
    activate A
    P-&gt;&gt;A: prompt("Using these primitive operations, suggest a way to do '...'")
    A-&gt;&gt;P: ...
    A-&gt;&gt;P: end_turn
    deactivate P
    deactivate A
    Note over P: 执行计划
    P-&gt;&gt;A: 工具返回结果
    deactivate P
  </pre>
<h2 id="conclusion">结论</h2>
<p>好的，本文勾勒了我对 <a href="https://agentclientprotocol.com/overview/introduction" rel="noopener noreferrer">ACP</a> 的一个变体，我称之为 SymmACP。SymmACP 扩展了 ACP，增加了</p>
<ul>
<li>允许任何一方（而不仅是服务器）提供对话初始状态的能力</li>
<li>允许“编辑器”向“智能体”提供 MCP 工具的能力</li>
<li>允许智能体在没有初始提示的情况下响应的能力</li>
<li>能够序列化对话并附加额外状态（这已经部分存在）的能力</li>
</ul>
<p>在我看来，这些扩展中的大多数都是对 ACP 的适度扩展，并且很容易通过添加新功能向后兼容地实现。<strong>但它们共同解锁了让任何人都能构建智能体扩展并以可组合方式部署的能力。</strong>我对此非常兴奋。这正是我希望 Symposium 所代表的。</p>
<p>值得注意一句老话：“能力越大，责任越大”。我一直谈论的这些代理和 ACP 层实际上就像 IDE 扩展。它们可以有效地做到<em>任何</em>你能做到的事情。存在明显的安全顾虑。不过我认为像微软的 <a href="https://opensource.microsoft.com/blog/2025/08/06/introducing-wassette-webassembly-based-tools-for-ai-agents/" rel="noopener noreferrer">Wassette</a> 这样的方法是关键——如果能有一个“基于能力”的概念来定义“代理层”，所有内容都编译为 WASM，并且用户可以调整给定代理<em>实际能</em>做什么，那就太棒了。</p>
<p>我计划在 <a href="https://symposium-dev.github.io/symposium/" rel="noopener noreferrer">Symposium</a> 和其他地方开始勾勒推动这项工作的计划。我的目标是建立一个完全开放且可互操作的客户端，它可以基于任何智能体（包括本地智能体），并且你可以挑选你想使用的任何部分。我预计会构建大量自定义功能来支持 Rust 开发（例如，使用新的 trait 求解器解释和诊断 trait 错误在我计划中优先级很高……还有宏错误……），但也会拥有其他功能，如引导、协作交互风格等，这些都是语言无关的——我也希望能看到针对其他语言的语言特性，特别是 Python 和 TypeScript（因为<a href="https://smallcultfollowing.com/babysteps/blog/2025/07/31/rs-py-ts-trifecta/" rel="noopener noreferrer">“新三驾马车”</a>），以及 Swift 和 Kotlin（因为移动端）。如果这个愿景让你兴奋，请加入 <a href="https://symposium-dev.zulipchat.com/" rel="noopener noreferrer">Symposium Zulip</a> 让我们聊聊！</p>
<h2 id="appendix-a-guide-to-the-agent-protocols-im-aware-of">附录：我所了解的智能体协议指南</h2>
<p>在讨论这个话题时，我经常被问到它与其他众多协议相比如何。让我简要概述一下相关工作以及我理解的它们的优缺点：</p>
<ul>
<li><em><a href="https://modelcontextprotocol.io/docs/getting-started/intro" rel="noopener noreferrer">模型上下文协议 (MCP)</a>:</em> 其中的王者。一个为智能体提供一组工具、提示和资源的协议。智能体可以通过提供适当的参数（JSON 格式）来调用工具。提示是用户可以使用特殊命令（如 <code>/</code> 或 <code>@</code>）调用的快捷方式，它们本质上是宏，可以“如同用户输入一样”展开（但它们也可以有参数并被动态构建）。<em>资源</em>只是可以请求的数据。MCP 服务器可以是本地的，也可以是远程托管的。远程 MCP 最近才成为一个选项，特别是在认证方面还很有限。
<ul>
<li>与 SymmACP 的比较：MCP 提供智能体可以调用的工具。SymmACP 在此之上构建，允许这些工具由代理链中的外层提供。SymmACP 面向控制整个聊天“体验”。</li>
</ul>
</li>
<li><em><a href="https://agentclientprotocol.com/overview/introduction" rel="noopener noreferrer">Zed 的智能体客户端协议 (ACP)</a>:</em> SymmACP 的基础。允许编辑器创建和管理会话。仅关注本地会话，因为你的编辑器在本地运行。
<ul>
<li>与 SymmACP 的比较：这正是本文讨论的内容！SymmACP 扩展了 ACP，增加了新功能，让中间层可以操纵历史记录、提供工具，并向上游提供扩展数据以支持比纯聊天更丰富的交互模式。另外，我预计我们可能想支持更多的远程能力，但这在我看来有点正交（例如，我希望能使用运行在云托管工作站上的智能体，但我可能会借助 ssh 来实现）。</li>
</ul>
</li>
<li><em><a href="https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/" rel="noopener noreferrer">谷歌的智能体对智能体协议 (A2A)</a> 和 <a href="https://www.ibm.com/think/topics/agent-communication-protocol" rel="noopener noreferrer">IBM 的智能体通信协议 (ACP)</a><sup id="fnref:5"><a href="#fn:5" rel="noopener noreferrer">5</a></sup>:</em> 据我了解，谷歌的“智能体对智能体”协议<em>有点</em>像 MCP 和 OpenAPI 的混合体。你可以 ping 运行在远程的智能体，让它们向你发送“智能体卡”，这些卡描述了它们可以执行的操作、如何认证以及其他类似内容。在我看来，它与 MCP 非常相似，只是它对远程执行的支持更丰富，特别是支持像长时间运行的通信这样的事情，其中智能体可能需要去工作一段时间，然后通过 webhook 反向 ping 你。
<ul>
<li><em>与 MCP 的比较：</em> 在我看来，A2A 看起来像是面向远程执行的 MCP 变体。MCP 有一个工具发现方法，你 ping 服务器以获取工具列表；A2A 有一个类似的机制，使用智能体卡。MCP 可以在本地运行，而 A2A 根据我的了解不行，但 A2A 在认证方面有更多的选项。MCP 只能同步调用，而 A2A 支持长时间运行的操作、进度更新和回调。看起来两者可以合并成一个整体。</li>
<li><em>与 SymmACP 的比较：</em> 我认为 A2A 与 SymmACP 是正交的。A2A 面向相互提供服务的智能体。SymmACP 面向为与智能体交互构建新的开发工具。你有可能可以基于 A2A 构建类似 SymmACP 的东西，但我不知道这样做真正能获得什么好处（并且我认为以后很容易做）。</li>
</ul>
</li>
</ul>
<div>
<hr>
<ol>
<li id="fn:1">
<p>每个人都以各种方式使用智能体。我喜欢 Simon Willison 的<a href="https://simonwillison.net/2025/May/22/tools-in-a-loop/" rel="noopener noreferrer">“智能体是在循环中使用模型的工具”</a>这个定义；我觉得“智能 CLI 工具”符合这个定义，只是循环的一部分是从用户那里读取输入。我认为“完全自主”的智能体是所有智能体的一个子集——许多智能体进程通过工具等与外部世界交互。从某个角度看，你可以将智能体“结束回合”视为调用一个工具，该工具的功能是“给我下一个提示”。&nbsp;<a href="#fnref:1" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:2">
<p>研究报告是我避免幻觉的一个<strong>重要</strong>部分。你可以在<a href="https://symposium-dev.github.io/symposium/research/lsp-overview/index.html" rel="noopener noreferrer">这里</a>看到我委托撰写的一份此类报告的示例；如果我们即将着手需要详细了解 LSP 的工作，我会让智能体先阅读那份报告。&nbsp;<a href="#fnref:2" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:3">
<p>或者：清除会话历史记录并重建它，但我更倾向于功能性的世界观，即给定会话永远不会改变。&nbsp;<a href="#fnref:3" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:4">
<p>我为 Q CLI 开始了一个实现，但分心了——而且，出于显而易见的原因，我开始对此失去兴趣。&nbsp;<a href="#fnref:4" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:5">
<p>是的，你没看错。还有另一个 ACP。谷歌搜索时有点容易混淆。=)&nbsp;<a href="#fnref:5" rel="noopener noreferrer">↩︎</a></p>
</li>
</ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 16481 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2025/10/08/symmacp/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2025/10/08/symmacp/</guid>
      <pubDate>Wed, 8 Oct 2025 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>我们需要（至少）符合人体工程学、显式的句柄</title>
      <category>ergonomic-rc</category>
      <description>[AI 摘要] 该文章主张应首先让显式句柄创建变得符合人体工程学，再考虑自动化，以满足不同应用场景的需求。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 该文章主张应首先让显式句柄创建变得符合人体工程学，再考虑自动化，以满足不同应用场景的需求。</div><p>在继续我关于“人体工程学引用计数”的讨论时，我想聚焦于核心问题：<strong>用户是否必须显式地调用 handle/clone，还是不应该？</strong> 整个“人体工程学引用计数”工作最初是由<a href="https://dioxuslabs.com/" rel="noopener noreferrer">Dioxus</a>提出的，他们的回答很简单：<strong>绝对不应该</strong>。对于他们正在构建的那类高级GUI应用程序而言，必须调用 <code>cx.handle()</code> 来克隆一个引用计数值纯粹是噪声。事实上，对于许多Rust应用来说，即使是克隆一个字符串或向量也没什么大不了的。另一方面，对于许多应用程序，答案是<strong>绝对应该</strong>——知道句柄创建的位置会影响性能、内存使用，甚至正确性（别担心，我稍后会在文章中举例说明）。那么我们如何调和这一点呢？</p>
<p><strong>本文主张，我们应该让“显式”变得符合人体工程学</strong>。这并非我一贯的立场，但在与Josh Triplett进行了一次有影响力的对话后，我转变了看法。我认为这符合我曾经所说的<a href="https://smallcultfollowing.com/babysteps//blog/2022/09/18/dyn-async-traits-part-8-the-soul-of-rust/" rel="noopener noreferrer">Rust的灵魂</a>：我们想要符合人体工程学，没错，但我们想要<strong>在保持控制力的同时符合人体工程学</strong><sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup>。</p>
<p>我喜欢Tyler Mandry提出的<em>“目标明确”</em>构念：*“伟大的代码只将你应用程序的重要特征带到你的注意力面前。”*关键在于，*存在这样的伟大代码，在其中克隆和句柄是重要的特征*，因此我们需要让这类代码能够被优雅地表达出来。这一点尤其重要，因为Rust是为数不多真正以那种低层、基础性代码为目标的语言之一。</p>
<p><strong>这并不意味着我们不能（稍后）支持自动的克隆和句柄。</strong>不可否认，这对许多Rust代码的目标明确性有益。但我认为我们应该<em>首先</em>关注更困难的情况，即需要显式性的那种情况，<strong>并尽力将其做得足够好</strong>；然后我们可以回头决定是否也支持某种自动化。事实上，对我来说，一个问题是我们是否能让“完全显式”变得<em>足够好</em>，以至于我们真的不需要自动版本。保持“一个Rust”有其好处，其中所有代码大致遵循相同的模式，这些模式在某些时候是完美的，而在它们过于繁琐时也不会太糟<sup id="fnref:2"><a href="#fn:2" rel="noopener noreferrer">2</a></sup>。</p>
<h2 id="rust-should-not-surprise-you-hat-tip-josh-triplett">“Rust不应让你感到意外。”（致谢：Josh Triplett）</h2>
<p>我提到这篇博文源于与Josh Triplett的一次长谈<sup id="fnref:3"><a href="#fn:3" rel="noopener noreferrer">3</a></sup>。那次对话中让我印象深刻的关键短语是：<em>Rust不应让你感到意外</em>。我的理解是这样的。每个程序员都知道那种马拉松式调试的感觉——坐着盯着代码看上几天，心里想着，<em>但是……这怎么可能发生呢？</em>这类问题排查可能以几种不同的方式结束。偶尔，你会发现逻辑中一个令人满足的微妙bug。更多时候，你发现你写了 <code>if foo</code> 而不是 <code>if !foo</code>。而<em>偶尔</em>，你会发现你的语言在做一些你意想不到的事情。一些看起来简单的代码隐藏着微妙而复杂的交互。人们常称这种为<em>陷阱</em>。</p>
<p>总体而言，Rust在避免陷阱方面做得<em>非常出色</em><sup id="fnref:4"><a href="#fn:4" rel="noopener noreferrer">4</a></sup>。我们实现这一目标的部分方法是确保你可能需要知道的信息是可见的——即在源代码中是显式的。每次你看到一个Rust的match语句，你都不必问自己“这里可能缺少哪些情况”——编译器保证所有情况都已覆盖。当你看到一个Rust函数调用时，你也不必问自己它是否会失败——如果可能失败，你会看到一个 <code>?</code><sup id="fnref:5"><a href="#fn:5" rel="noopener noreferrer">5</a></sup>。</p>
<h3 id="creating-a-handle-can-definitely-surprise-you">创建句柄绝对可能“让你感到意外”</h3>
<p>所以我想问题在于：<em>你是否曾经需要知道引用计数的增加？</em>棘手之处在于答案取决于应用。对于一些底层应用，绝对需要：原子引用计数是可衡量的开销。老实说，我敢打赌存在这种需求的应用集非常小。即使在那些应用中，Rust通过让你能够在 <code>Rc</code> 和 <code>Arc</code> 之间进行选择<em>并证明你不会搞砸</em>，已经比当前技术有所改进。</p>
<p>但还有其他原因你可能想要跟踪引用计数，这些原因更不容忽视。其中之一是内存泄漏。与垃圾回收语言不同，Rust具有<em>确定性析构</em>。这很酷，因为这意味着你可以利用析构函数来管理各种资源，正如Yehuda很久以前在他关于<a href="https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization" rel="noopener noreferrer">RAII</a>的经典颂歌<a href="https://blog.skylight.io/rust-means-never-having-to-close-a-socket/" rel="noopener noreferrer">“Rust意味着永远不必关闭套接字”</a>中所写的那样。但是，尽管句柄创建和销毁的点是确定的，引用计数的本质使得预测底层资源何时真正被释放变得更加困难。如果这些增量在代码中不可见，就更难追踪它们了。</p>
<p>就在最近，我调试了用Swift编写的<a href="" rel="noopener noreferrer">Symposium</a>。不知何故，我本应只有一个 <code>IPCManager</code> 实例，却出现了两个，而且每个实例都在响应每条IPC消息，造成了混乱。在翻找中，我在一些意想不到的地方发现了游离的引用，这就是问题的根源。如果我必须显式地编写 <code>.handle()</code> 来增加引用计数，这个bug是否还会发生？绝对会。事后是否更容易找到？也是肯定的<sup id="fnref:6"><a href="#fn:6" rel="noopener noreferrer">6</a></sup>。</p>
<p>Josh给了我一个来自<a href="https://docs.rs/bytes/latest/bytes/" rel="noopener noreferrer">“bytes” crate</a>的类似例子。<a href="https://docs.rs/bytes/latest/bytes/struct.Bytes.html" rel="noopener noreferrer"><code>Bytes</code></a>类型是<a href="https://smallcultfollowing.com/babysteps/blog/2025/10/07/the-handle-trait/" rel="noopener noreferrer">指向某个底层内存缓冲区的句柄</a>。当你克隆这个句柄时，它会保留整个后备缓冲区。有时你可能更愿意将你的切片复制到一个单独的缓冲区中，以便底层缓冲区可以被释放。我不难想象，试图追踪一个让某个大缓冲区保持存活的错误句柄，并且因为无法在代码中显式地看到这些句柄创建的位置而感到非常沮丧。</p>
<p>类似的案例也出现在像 <code>Arc::get_mut</code><sup id="fnref:7"><a href="#fn:7" rel="noopener noreferrer">7</a></sup> 这样的API中。 <code>get_mut</code> 接受一个 <code>&amp;mut Arc&lt;T&gt;</code>，如果引用计数为1，则返回一个 <code>&amp;mut T</code>。这让你能够获取一个<em>可共享</em>的句柄（你确信它实际上并<em>未</em>被共享），并恢复其唯一性。这类API并不常用——但当你需要它时，它就在那里，非常棒。</p>
<h2 id="what-i-love-about-rust-is-its-versatility-low-to-high-in-one-language-hat-tip-alex-crichton">“我喜欢Rust是因为它的多功能性：一门语言涵盖底层到高层”（致谢：Alex Crichton）</h2>
<p>在与Josh的对话开始时，我倾向于一种设计，其中句柄有某种形式的自动克隆，以及一个默认允许的lint，让那些<em>不</em>想要这样做的crate可以将其关闭。但Josh说服我，存在相当一部分应用程序，它们希望句柄创建既符合人体工程学又可见（即在源代码中是显式的）。底层网络服务，甚至像Rust for Linux这样的项目可能都属于此类，但任何使用 <code>get_mut</code> 或 <code>make_mut</code> 的Rust应用也可能如此。</p>
<p>这让我想起Alex Crichton曾经对我说过的话。与这里的其他引用不同，它并非在人体工程学引用计数的语境下，而是在我致力于<a href="https://smallcultfollowing.com/babysteps/blog/2021/09/08/rustacean-principles/" rel="noopener noreferrer">“Rust使用者原则”</a>的首次尝试时。Alex当时说，他喜欢Rust不仅对底层代码很棒，对CLI工具和简单脚本等高层内容也同样适用。</p>
<p>我觉得你可以从两个方面解读Alex的引言，取决于你强调什么。你可以将其理解为，“Rust对高层用例有益这一点很重要。”这是真的，也正是它促使我们思考是否应该让句柄可见。</p>
<p>但你也可以将Alex的引言解读为，“有一门语言能<em>同时</em>很好地满足<em>两者</em>的需求，这一点很重要”——我认为这也是对的。“真正的Rust整体精神”在于我们成功地<em>同时</em>赋予你底层代码所需的低层控制，又将其包裹在高层的包装中。这当然是零成本抽象的承诺，而Rust（在最佳时刻）实现了这一点。</p>
<h3 id="the-soul-of-rust-low-level-enough-for-a-kernel-usable-enough-for-a-gui">“Rust的灵魂”：底层到足以编写内核，易用到足以构建GUI</h3>
<p>老实说，高级GUI编程并非Rust的强项，也永远不会是；用户永远不会将Rust与TypeScript混淆。但同样，TypeScript也永远不会出现在Linux内核中。</p>
<p>Rust的目标是成为一门大体上能对<em>两者</em>都“足够好”的语言。<strong>目标是向内核黑客暴露足够多的底层细节，同时以一种对GUI足够易用的方式来实现。</strong>这并非易事，但这是职责所在。</p>
<p>这不是Josh第一次将我拉回这个认识。上一次是在讨论dyn trait中的async fn时，它促成了一篇讨论<a href="https://smallcultfollowing.com/babysteps/blog/2022/09/18/dyn-async-traits-part-8-the-soul-of-rust/" rel="noopener noreferrer">“Rust的灵魂”</a>的博文以及一篇<a href="https://smallcultfollowing.com/babysteps/blog/2022/09/19/what-i-meant-by-the-soul-of-rust/" rel="noopener noreferrer">更详细阐述的后续文章</a>。我认为“底层到足以编写内核，易用到足以构建GUI”这句口号很好地概括了这一点。</p>
<h3 id="conclusion-explicit-handles-should-be-the-first-step-but-it-doesnt-have-to-be-the-final-step">结论：显式句柄应该是第一步，但不一定是最后一步</h3>
<p>我想补充一个小告诫。我认为Rust灵魂的另一部分是<em>偏爱细微差别而非人为的简单化</em>（“尽可能简单，但不要过于简单”，正如他们所说）。现实情况是，有大量的应用程序（尤其但不限于异步领域<sup id="fnref:8"><a href="#fn:8" rel="noopener noreferrer">8</a></sup>）会到处创建新句柄，而显式创建新句柄是噪声，而非信号。这就是为什么例如Swift<sup id="fnref:9"><a href="#fn:9" rel="noopener noreferrer">9</a></sup>让引用计数增量不可见——他们从中获益良多！<sup id="fnref:10"><a href="#fn:10" rel="noopener noreferrer">10</a></sup>我敢打赌大多数Swift用户甚至没有意识到Swift不是垃圾回收的<sup id="fnref:11"><a href="#fn:11" rel="noopener noreferrer">11</a></sup>。</p>
<p>但关键在于，即使我们添加某种方式使句柄创建自动化，我们<em>也</em>希望有一个模式是显式且可见的。所以我们不妨先实现这个。</p>
<p>好的，我想我已经从三个角度阐述了这个观点，就此打住。本系列的接下来的几篇博文将深入探讨（至少）两种选项，关于如何在保持显式性的同时，让句柄创建和闭包变得更符合人体工程学。</p>
<div>
<hr>
<ol>
<li id="fn:1">
<p>我看到一个可能的候选设计原则……<em>搓着手，发出邪恶般的窃笑，面露喜色</em>&nbsp;<a href="#fnref:1" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:2">
<p><a href="https://youtu.be/JMFS9lrVd64?si=BdaDNm7rIueS0Jlx&amp;t=71" rel="noopener noreferrer">这是个行业术语</a>。&nbsp;<a href="#fnref:2" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:3">
<p>实际上，按照我和Josh经常进行的对话标准，这并不算特别长——最多一小时。&nbsp;<a href="#fnref:3" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:4">
<p>嗯，至少<em>同步</em>Rust是这样。我认为异步Rust可能有更多问题，尤其是在取消方面，但那是另一篇博文的主题。&nbsp;<a href="#fnref:4" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:5">
<p>当然，不包括panic——而且accounting for panic（处理panic）对某些Rust用户来说是个主要痛点，这并不奇怪。&nbsp;<a href="#fnref:5" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:6">
<p>在这个特定案例中，无论如何我都比较容易找到，但这个应用程序非常简单。我绝对可以想象在代码库中使用ripgrep查找所有增量会很有用，而如果没有显式的信号表明它们正在发生，这将困难得多。&nbsp;<a href="#fnref:6" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:7">
<p>或者 <code>Arc::make_mut</code>，这是我最喜欢的API之一。它接受一个 <code>Arc&lt;_&gt;</code>，并始终给你返回内部的可变（即唯一）访问权限！这怎么可能呢，即使引用计数可能不是1？答案是：如果引用计数不是1，那么它就会克隆。这非常适合写时复制风格的代码。太美了。😍&nbsp;<a href="#fnref:7" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:8">
<p>我的经验是，由于我们确实应该修复的语言限制，许多异步结构强迫你使用 <code>'static</code> 约束，这反过来又迫使你在原本可以使用 <code>&amp;</code> 的地方使用 <code>Rc</code> 和 <code>Arc</code>。&nbsp;<a href="#fnref:8" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:9">
<p>我一直在写更多Swift并且很享受。我得说，我喜欢他们不害怕“大刀阔斧”。我欣赏我在SwiftUI和他们的异步方法设计中看到的雄心。我不认为他们能十全十美，但他们敢于尝试很酷。我希望Rust也能<a href="https://smallcultfollowing.com/babysteps/blog/2022/02/09/dare-to-ask-for-more-rust2024/" rel="noopener noreferrer">敢于要求更多</a>！&nbsp;<a href="#fnref:9" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:10">
<p>嗯，不<em>仅仅</em>是那样。他们还允许类字段在别名化时被赋值，而为了避免陈旧引用和迭代器失效，这意味着你必须将所有东西移入引用计数的框并采用持久化集合，而这又带来了性能开销，并使得Swift更难成为底层基础系统的候选（尽管我认为并非不可行）。&nbsp;<a href="#fnref:10" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:11">
<p>不过我也敢打赌，许多人最终会发现自己对一个引用计数循环感到困惑。我还没有深入研究Swift如何处理这些，但我看到有提到“弱句柄”，所以我假设他们（可能）还没有采用循环收集器。需要明确的是，在Rust中也可能出现引用计数循环！因为我们不鼓励内部可变性，所以更难做到，但并非不可能。&nbsp;<a href="#fnref:11" rel="noopener noreferrer">↩︎</a></p>
</li>
</ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 9633 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2025/10/13/ergonomic-explicit-handles/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2025/10/13/ergonomic-explicit-handles/</guid>
      <pubDate>Mon, 13 Oct 2025 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>移动、析构、遗忘与Rust</title>
      <category>must-move</category>
      <description>[AI 摘要] 该文章提出在Rust中引入层次化特质以控制值的丢弃、移动和遗忘能力，以支持异步析构等高级模式。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 该文章提出在Rust中引入层次化特质以控制值的丢弃、移动和遗忘能力，以支持异步析构等高级模式。</div><p>本文提出了一项扩展 Rust 以支持多种不同析构器的提案。这意味着我们可以实现异步丢弃，同时也能防止值被“遗忘”（泄漏），从而支持类似 rayon/libstd 风格的并行异步作用域任务。我们还将能够定义其“析构器”需要参数的类型。这一提案——我称之为“受控析构”——是我之前提出的<a href="https://smallcultfollowing.com/babysteps/blog/2023/03/16/must-move-types.html" rel="noopener noreferrer">“必须移动”</a>概念的演进。我认为，要让 Rust 实现其系统编程关键模式安全版本的目标，这是必要的。因此，这也是实现“异步梦想”所必需的，即让异步 Rust 和同步 Rust 大致工作方式相同。</p>
<p>如此好事不会凭空而来。该提案的主要代价是它在 Rust 类型中引入了更多“核心划分”。我相信这些划分动机充分且合理——它们反映了内在的复杂性——尽管如此，我们仍需仔细思考。</p>
<h2 id="summary">摘要</h2>
<p>提案的要点在于，我们应该：</p>
<ul>
<li>引入一个新的“默认 trait 约束” <code>Forget</code> 及其关联的 trait 层次结构：
<ul>
<li><code>trait Forget: Drop</code>，表示可以被“遗忘”的值。</li>
<li><code>trait Destruct: Move</code>，表示具有析构器的值。</li>
<li><code>trait Move: Pointee</code>，表示可以被移动的值。</li>
<li><code>trait Pointee</code>，基础 trait，表示任何值。</li>
</ul>
</li>
<li>使用类似 <a href="https://github.com/rust-lang/rfcs/pull/3729" rel="noopener noreferrer">RFC #3729（Sized 层次结构）</a> 中为 Sized 特性提出的“选择退出较弱默认值”方案。
<ul>
<li>因此，<code>fn foo&lt;T&gt;(t: T)</code> 默认意为“一个可以被遗忘/析构/移动的 <code>T</code>”。</li>
<li>而 <code>fn foo&lt;T: Destruct&gt;(t: T)</code> 则表示“一个可以被析构，但不一定能被遗忘的 <code>T</code>”。</li>
<li>而 <code>fn foo&lt;T: Move&gt;(t: T)</code> 表示“一个可以被移动，但不一定能被遗忘的 <code>T</code>”。</li>
<li>……以此类推。</li>
</ul>
</li>
<li>集成并执行新的 trait：
<ul>
<li><code>std::mem::forget</code> 的约束将已经要求 <code>Forget</code>，这很好。</li>
<li>借用检查器可以强制要求任何被丢弃的值必须实现 <code>Destruct</code>；事实上，我们已经在 <code>const fn</code> 中通过此方式来强制 <code>const Destruct</code> 约束。</li>
<li>借用检查器可以扩展，要求任何被移动的值具有 <code>Move</code> 约束。</li>
</ul>
</li>
<li>调整闭包的 trait 约束（幸运的是，这相当容易实现）。</li>
</ul>
<h2 id="motivation">动机</h2>
<p>在我几年前于乌拉圭 Rust LATAM 大会上的一次<a href="https://nikomatsakis.github.io/rust-latam-2019/#1" rel="noopener noreferrer">演讲</a><sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup>中，我<a href="https://nikomatsakis.github.io/rust-latam-2019/#81" rel="noopener noreferrer">这样说过</a>：</p>
<ul>
<li><strong>暴露</strong>高性能 API 很容易。</li>
<li>但<strong>帮助用户控制它</strong>却很难——而这正是 Rust 类型系统所做的。</li>
</ul>
<img src="https://smallcultfollowing.com/babysteps/assets/2025-movedestructleak/firespell.gif" alt="一个人施展火系法术并烧伤自己">
<p>Rust 目前在防止程序各部分相互干扰方面做得相当好，但在保证清理工作发生<sup id="fnref:2"><a href="#fn:2" rel="noopener noreferrer">2</a></sup>方面做得不够。我们当然有析构器，但它们有两个关键限制：</p>
<ul>
<li>所有析构器必须符合相同的签名 <code>fn drop(&amp;mut self)</code>，这并不总是足够的。</li>
<li>一旦放弃值的所有权，就无法保证析构器会运行。</li>
</ul>
<h3 id="making-it-concrete">具体化</h3>
<p>上述动机较为抽象，让我给出一些与此局限相关的具体示例：</p>
<ul>
<li>支持 <code>async</code> 或 <code>const</code> 丢弃的能力，两者都需要不同的丢弃签名。</li>
<li>支持一个需要参数的“丢弃”操作，例如必须发送的消息，或在程序终止前必须提供的结果代码。</li>
<li>支持可以访问栈的异步作用域，这需要一种方式来保证并行线程在异步上下文中也能被 join。</li>
<li>以最大效率与 WebAssembly 异步任务集成的能力，这需要保证清理<sup id="fnref:3"><a href="#fn:3" rel="noopener noreferrer">3</a></sup>。</li>
</ul>
<p>本文的目标是概述一种可能解决上述所有问题且与当前 Rust 向后兼容的方法。</p>
<h3 id="the-capabilities-of-value-disposal">值处置的“能力”</h3>
<p>核心问题在于，Rust 今天假设每个 <code>Sized</code> 的值都可以被移动、丢弃和遗忘：</p>
<div><pre><code><span><span><span>// 除了知道 `T` 是 `Sized` 的，我们还能...
</span></span></span><span><span><span>fn</span> <span>demonstration</span><span>&lt;</span><span>T</span><span>&gt;</span><span>(</span><span>a</span>: <span>T</span><span>,</span><span> </span><span>b</span>: <span>T</span><span>,</span><span> </span><span>c</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>std</span>::<span>mem</span>::<span>drop</span><span>(</span><span>a</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>std</span>::<span>mem</span>::<span>forget</span><span>(</span><span>b</span><span>);</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>    </span><span>// ...移动 `c` 到 `x`
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>x</span><span> </span><span>=</span><span> </span><span>c</span><span>;</span><span>
</span></span></span><span><span><span>}</span><span> </span><span>// ...然后当离开块时，让 `x` 被自动丢弃。
</span></span></span></code></pre></div><h3 id="destructors-are-like-opt-out-methods">析构器就像“选择退出方法”</h3>
<p>在我看来，大多数方法是“选择加入”的——除非你调用它们，否则不会执行。但析构器不同。它们实际上是一种默认运行的方法——除非你选择退出，例如通过调用 <code>forget</code>。但选择退出的能力意味着它们本质上不比普通方法增加任何功能，只是让 API 更人性化。</p>
<p>这意味着，在当前 Rust 中，<em>保证</em>析构器会运行的唯一方式是保留值的所有权。这对不安全代码可能很重要——例如，允许作用域线程的 API 需要<em>保证</em>这些并行线程在函数返回前被 join。他们实现这一点的唯一方式是使用一个对 <code>scope</code> 提供 <code>&amp;</code> 借用访问的闭包：</p>
<div><pre><code><span><span><span>scope</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>// 这个值的类型是 `&amp;Scope`...
</span></span></span></code></pre></div><p>因为该 API 从不放弃作用域的所有权，所以它可以确保它永远不会被“遗忘”，从而保证其析构器会运行。</p>
<p>作用域线程方法适用于同步代码，但不适用于异步代码。问题在于异步函数返回一个 future，它是一个值。因此用户可以选择“遗忘”这个值，就像任何其他值一样，从而导致析构器可能永远不会运行。</p>
<h3 id="guaranteed-cleanup-is-common-in-systems-programming">保证清理在系统编程中很常见</h3>
<p>当你开始探索时，会发现<em>保证的</em>析构器在系统编程中相当常见。future 中的作用域 API 是一个例子，但 DMA（直接内存访问）是另一个。许多嵌入式设备有一种模式，你开始一次 DMA 传输，导致内存异步写入。但你需要确保在释放该内存<em>之前</em>终止此 DMA。如果该内存在你的栈上，这意味着你需要一个析构器来取消 DMA 或阻塞直到完成<sup id="fnref:4"><a href="#fn:4" rel="noopener noreferrer">4</a></sup>。</p>
<h2 id="so-what-can-we-do-about-it">那么我们能做什么？</h2>
<p>这种情况与重新审视默认 <code>Sized</code> 约束的挑战非常类似，我认为我在[这篇博文][sized]中概述的相同基本方法会奏效。</p>
<p>核心思想很简单：有一组“特殊”的 trait 构成层次结构：</p>
<div><pre><code><span><span><span>trait</span><span> </span><span>Forget</span>: <span>Destruct</span><span> </span><span>{}</span><span> </span><span>// 可以被“遗忘”
</span></span></span><span><span><span>trait</span><span> </span><span>Destruct</span>: <span>Move</span><span> </span><span>{}</span><span>   </span><span>// 可以被“析构”（丢弃）
</span></span></span><span><span><span>trait</span><span> </span><span>Move</span>: <span>Pointee</span><span> </span><span>{}</span><span>    </span><span>// 可以被“移动”
</span></span></span><span><span><span>trait</span><span> </span><span>Pointee</span><span> </span><span>{}</span><span>          </span><span>// 可以被指针引用
</span></span></span></code></pre></div><p>默认情况下，泛型参数会获得 <code>Forget</code> 约束，因此 <code>fn foo&lt;T&gt;()</code> 等同于 <code>fn foo&lt;T: Forget&gt;()</code>。但如果参数<em>选择</em>一个更弱的约束，则默认值会被抑制，所以 <code>fn bar&lt;T: Destruct&gt;()</code> 意味着 <code>T</code> 被假定为“可析构的”但<em>不是</em>可遗忘的。而 <code>fn baz&lt;T: Move&gt;()</code> 表示 <code>T</code> 只能被移动。</p>
<h2 id="impact-of-these-bounds">这些约束的影响</h2>
<p>让我简要解释这些约束将如何工作。</p>
<h3 id="the-default-can-forget-drop-move-etc">默认可以遗忘、丢弃、移动等</h3>
<p>给定一个默认类型 <code>T</code>，或者显式写 <code>Forget</code> 的类型，函数可以执行当前可能的所有操作：</p>
<div><pre><code><span><span><span>fn</span> <span>just_forget</span><span>&lt;</span><span>T</span>: <span>Forget</span><span>&gt;</span><span>(</span><span>a</span>: <span>T</span><span>,</span><span> </span><span>b</span>: <span>T</span><span>,</span><span> </span><span>c</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>std</span>::<span>mem</span>::<span>drop</span><span>(</span><span>a</span><span>);</span><span>   </span><span>// 正常
</span></span></span><span><span><span>    </span><span>std</span>::<span>mem</span>::<span>forget</span><span>(</span><span>b</span><span>);</span><span> </span><span>// 正常
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>x</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></code></pre></div><h3 id="the-forget-function-requires-t-forget">forget 函数要求 <code>T: Forget</code></h3>
<p><code>std::mem::forget</code> 函数也将要求 <code>T: Forget</code>：</p>
<div><pre><code><span><span><span>pub</span><span> </span><span>fn</span> <span>forget</span><span>&lt;</span><span>T</span>: <span>Forget</span><span>&gt;</span><span>(</span><span>value</span>: <span>T</span><span>)</span><span> </span><span>{</span><span> </span><span>/* magic intrinsic */</span><span> </span><span>}</span><span>
</span></span></span></code></pre></div><p>这意味着如果你只有 <code>Destruct</code>，函数只能丢弃或移动，不能“遗忘”：</p>
<div><pre><code><span><span><span>fn</span> <span>just_destruct</span><span>&lt;</span><span>T</span>: <span>Destruct</span><span>&gt;</span><span>(</span><span>a</span>: <span>T</span><span>,</span><span> </span><span>b</span>: <span>T</span><span>,</span><span> </span><span>c</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>// 此函数仅请求“Destruct”能力。
</span></span></span><span><span><span>
</span></span></span><span><span><span>    </span><span>std</span>::<span>mem</span>::<span>drop</span><span>(</span><span>a</span><span>);</span><span>   </span><span>// 正常
</span></span></span><span><span><span>    </span><span>std</span>::<span>mem</span>::<span>forget</span><span>(</span><span>b</span><span>);</span><span> </span><span>// 错误：需要 `T: Forget`
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>x</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></code></pre></div><h3 id="the-borrow-checker-would-require-dropped-values-implement-destruct">借用检查器将要求“丢弃”的值实现 <code>Destruct</code></h3>
<p>我们将修改 <code>drop</code> 函数，仅要求 <code>T: Destruct</code>：</p>
<div><pre><code><span><span><span>fn</span> <span>drop</span><span>&lt;</span><span>T</span>: <span>Destruct</span><span>&gt;</span><span>(</span><span>t</span>: <span>T</span><span>)</span><span> </span><span>{}</span><span>
</span></span></span></code></pre></div><p>我们还将扩展借用检查器，使其在看到值被丢弃（即，因为它离开作用域）时，要求 <code>Destruct</code> 约束。</p>
<p>这意味着如果你有一个类型仅实现 <code>Move</code> 的值，你无法“丢弃”它：</p>
<div><pre><code><span><span><span>fn</span> <span>just_move</span><span>&lt;</span><span>T</span>: <span>Move</span><span>&gt;</span><span>(</span><span>a</span>: <span>T</span><span>,</span><span> </span><span>b</span>: <span>T</span><span>,</span><span> </span><span>c</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>// 此函数仅请求“Move”能力。
</span></span></span><span><span><span>
</span></span></span><span><span><span>    </span><span>std</span>::<span>mem</span>::<span>drop</span><span>(</span><span>a</span><span>);</span><span>   </span><span>// 错误：需要 `T: Destruct`
</span></span></span><span><span><span>    </span><span>std</span>::<span>mem</span>::<span>forget</span><span>(</span><span>b</span><span>);</span><span> </span><span>// 错误：需要 `T: Forget`
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>x</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>// 错误：`x` 正在被丢弃，但 `T: Destruct`
</span></span></span></code></pre></div><p>这意味着如果你只有 <code>Move</code> 约束，你必须移动你拥有的任何东西才能从函数返回。例如：</p>
<div><pre><code><span><span><span>fn</span> <span>return_ok</span><span>&lt;</span><span>T</span>: <span>Move</span><span>&gt;</span><span>(</span><span>a</span>: <span>T</span><span>)</span><span> </span>-&gt; <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>}</span><span>
</span></span></span></code></pre></div><p>如果你有一个不移动的函数，你会得到一个错误：</p>
<div><pre><code><span><span><span>fn</span> <span>return_err</span><span>&lt;</span><span>T</span>: <span>Move</span><span>&gt;</span><span>(</span><span>a</span>: <span>T</span><span>)</span><span> </span>-&gt; <span>T</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>}</span><span> </span><span>// 错误：`a` 未实现 `Destruct`
</span></span></span></code></pre></div><p>值得指出的是，在 panic 面前，这将会非常恼人：</p>
<div><pre><code><span><span><span>fn</span> <span>return_err</span><span>&lt;</span><span>T</span>: <span>Move</span><span>&gt;</span><span>(</span><span>a</span>: <span>T</span><span>)</span><span> </span>-&gt; <span>T</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>// 错误：如果发生 panic，`a` 将被丢弃，但 `T` 未实现 `Destruct`
</span></span></span><span><span><span>    </span><span>forbid_env_var</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>fn</span> <span>forbid_env_var</span><span>()</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>if</span><span> </span><span>std</span>::<span>env</span>::<span>var</span><span>(</span><span>"BAD"</span><span>).</span><span>is_ok</span><span>()</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>panic!</span><span>(</span><span>"Uh oh: BAD cannot be set"</span><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></pre></div><p>我对此可以接受，但这将迫使我们寻求更好的方式来静态排除 panic。</p>
<h3 id="const-and-later-async-variants-of-destruct">const（以及未来的 async）<code>Destruct</code> 变体</h3>
<p>实际上，我们已经在 const 函数中做了类似丢弃检查的事情。现在如果你有一个 const fn 并尝试丢弃一个值，你会得到一个错误：</p>
<div><pre><code><span><span><span>const</span><span> </span><span>fn</span> <span>test</span><span>&lt;</span><span>T</span><span>&gt;</span><span>(</span><span>t</span>: <span>T</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>}</span><span> </span><span>// 错误！
</span></span></span></code></pre></div><p>编译它会产生错误：</p>
<pre><code>error[E0493]: destructor of `T` cannot be evaluated at compile-time
 --&gt; src/lib.rs:1:18
  |
1 | const fn test&lt;T&gt;(t: T) { }
  |                  ^       - value is dropped here
  |                  |
  |                  the destructor for this type cannot be evaluated in constant functions
</code></pre><p>目前这个检查并未在借用检查器中进行，但它可以。</p>
<h3 id="the-borrow-checker-would-require-moved-values-implement-move">借用检查器将要求“移动”的值实现 <code>Move</code></h3>
<p>检查的最后一部分将是要求“移动”的值实现 <code>Move</code>：</p>
<div><pre><code><span><span><span>fn</span> <span>return_err</span><span>&lt;</span><span>T</span>: <span>Pointee</span><span>&gt;</span><span>(</span><span>a</span>: <span>T</span><span>)</span><span> </span>-&gt; <span>T</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>a</span><span> </span><span>// 错误：`a` 未实现 `Move`
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>你可能认为拥有 <code>!Move</code> 的类型会取代对 pin 的需要，但事实并非如此。一个<em>被固定的</em>值是<em>永远不能再移动</em>的值，而一个不是 <code>Move</code> 的值则根本无法被移动——至少一旦它被存储到一个位置后是这样。</p>
<p>我不确定提案的这部分是否有意义，我们可以先让所有类型都是 <code>Move</code>、<code>Destruct</code> 或（默认的）<code>Forget</code>。</p>
<h3 id="opting-out-from-forget-etc">选择退出遗忘等</h3>
<p>提案的另一部分是你应该能够显式“选择退出”遗忘能力，例如通过这样做：</p>
<div><pre><code><span><span><span>struct</span><span> </span><span>MyType</span><span> </span><span>{}</span><span>
</span></span></span><span><span><span>impl</span><span> </span><span>Destruct</span><span> </span><span>for</span><span> </span><span>MyType</span><span> </span><span>{}</span><span>
</span></span></span></code></pre></div><p>这样做当然会限制可以接受你类型的泛型。</p>
<h3 id="associated-type-bounds">关联类型约束</h3>
<p>这些“默认约束”提案最棘手的部分总是关联类型约束。为了向后兼容，我们必须默认到 <code>Forget</code>，但许多现有关联类型实际上并不<em>需要</em> <code>Forget</code>。例如，像 <code>Add</code> 这样的 trait 其返回类型其实应该只要求 <code>Move</code>：</p>
<div><pre><code><span><span><span>trait</span><span> </span><span>Add</span><span>&lt;</span><span>Rhs</span><span> </span><span>=</span><span> </span><span>Self</span><span>&gt;</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>type</span> <span>Output</span><span> </span><span>/* : Move */</span><span>;</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>我基本上不太担心这个。我们可能随时间或通过版本削弱这些约束。或者，也许添加某种版本特定的“别名”，如：</p>
<div><pre><code><span><span><span>trait</span><span> </span><span>Add2025</span><span>&lt;</span><span>Rhs</span><span> </span><span>=</span><span> </span><span>Self</span><span>&gt;</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>type</span> <span>Output</span>: <span>Move</span><span>;</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>其中 <code>Add2025</code> 为所有实现了 <code>Add</code> 的类型实现。</p>
<p>我不确定具体如何管理，但我们会弄清楚的——与此同时，大多数不应被遗忘的类型其实只是不必流经太多地方的“守卫”类型。</p>
<h4 id="associated-type-bounds-in-closures">闭包中的关联类型约束</h4>
<p>我认为一个<em>确实重要</em>需要削弱关联类型约束的地方是闭包——幸运的是，由于我们的“闭包 trait 约束”语法的工作方式，这是我们可以做到的地方。我记得我之前写过一篇关于此的文章，但现在找不到了，简短版本是：今天，当你写 <code>F: Fn()</code> 时，意味着闭包必须返回 <code>()</code>。如果你写 <code>F: Fn() -&gt; T</code>，那么这个类型 <code>T</code> 必须在其他地方声明，因此 <code>T</code> 将（独立于 <code>Fn</code> trait 的关联类型）获得一个默认的 <code>Forget</code> 约束。因此，由于 <code>Fn</code> 关联类型在稳定 Rust 中不能独立命名，我们可以更改其约束，而像这样的代码将继续正常工作：</p>
<div><pre><code><span><span><span>fn</span> <span>foo</span><span>&lt;</span><span>T</span><span>,</span><span> </span><span>F</span><span>&gt;</span><span>()</span><span>
</span></span></span><span><span><span>where</span><span>
</span></span></span><span><span><span>    </span><span>F</span>: <span>Fn</span><span>()</span><span> </span>-&gt; <span>T</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>//         - `T: Forget` 仍默认成立
</span></span></span><span><span><span>{}</span><span>
</span></span></span></code></pre></div><h2 id="frequently-asked-questions">常见问题</h2>
<h3 id="how-does-this-relate-to-the-recent-thread-on-internals">这与最近在 internals 上的讨论有何关联？</h3>
<p>最近有人向我指出了<a href="https://internals.rust-lang.org/t/pre-rfc-substructural-type-system/23614" rel="noopener noreferrer">这个 internals 讨论</a>，关于一个“子结构类型系统”，它可能具有非常相似的能力。老实说，我还没有时间去阅读和消化它！但我这篇博文已经完成了 95%，所以我决定先发布它，然后再去尝试比较。</p>
<h3 id="what-would-it-mean-for-a-struct-to-opt-out-of-move-eg-by-being-only-pointee">结构体选择退出 <code>Move</code>（例如，仅实现 <code>Pointee</code>）意味着什么？</h3>
<p>因此，我描述的系统<em>会</em>允许“不可移动”类型（即选择退出所有功能，只允许 <code>Pointee</code> 的结构体），但这样的结构体实际上只能存储在静态内存位置。你不能把它放在栈上，因为栈最终必须被弹出。你也不能把它从一个位置移动到另一个位置，因为，它是不可移动的。</p>
<p>这似乎是有用的——例如，建模“视频 RAM”或生活在内存特定位置而不能在其他任何地方的东西——但这并非广泛需求。</p>
<h3 id="how-would-you-handle-destructors-with-arguments">如何处理带参数的析构器？</h3>
<p>我想象类似这样的东西：</p>
<div><pre><code><span><span><span>struct</span><span> </span><span>Transaction</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>data</span>: <span>Vec</span><span>&lt;</span><span>u8</span><span>&gt;</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>/// 选择退出析构
</span></span></span><span><span><span>impl</span><span> </span><span>Move</span><span> </span><span>for</span><span> </span><span>Transaction</span><span> </span><span>{</span><span> </span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>impl</span><span> </span><span>Transaction</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>// 这实际上是一个“析构器”
</span></span></span><span><span><span>    </span><span>pub</span><span> </span><span>fn</span> <span>complete</span><span>(</span><span>
</span></span></span><span><span><span>        </span><span>self</span><span>,</span><span>
</span></span></span><span><span><span>        </span><span>connection</span>: <span>Connection</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>let</span><span> </span><span>Transaction</span><span> </span><span>{</span><span> </span><span>data</span><span> </span><span>}</span><span> </span><span>=</span><span> </span><span>self</span><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></pre></div><p>通过这种设置，任何拥有 <code>Transaction</code> 的函数都必须最终调用 <code>transaction.complete()</code>。这是因为这种类型的值不能被丢弃，所以它们必须被移动。</p>
<h3 id="how-does-this-relate-to-async-drop">这与异步丢弃有何关联？</h3>
<p>这种设置解决了阻碍我心目中异步丢弃的一个关键问题，即“异步丢弃”的类型不必实现“同步丢弃”。这使得类型系统能够防止它们在同步代码中被丢弃，这意味着它们只能在异步丢弃中被丢弃。但那里仍然有大量的设计工作要做。</p>
<h3 id="why-is-the-trait-destruct-and-not-drop">为什么 trait 是 <code>Destruct</code> 而不是 <code>Drop</code>？</h3>
<p>这来自 const 泛型的工作。我不喜欢这样。但有其逻辑。现在，当你丢弃一个结构体或其他值时，实际上会执行一系列操作，其中只有一项是运行任何 <code>Drop</code> 实现——例如，它还会递归丢弃结构体中的所有字段等。其思想是“析构”指的是这整个序列。</p>
<h3 id="how-hard-would-this-to-be-to-prototype">原型实现有多难？</h3>
<p>我……实际上认为不会很难。我已经思考过一些，所有这些更改似乎都相当直接。我很乐意支持一个<a href="https://lang-team.rust-lang.org/how_to/experiment.html" rel="noopener noreferrer">语言团队实验</a>。</p>
<h3 id="does-this-mean-we-should-have-had-leak">这是否意味着我们应该有 leak？</h3>
<p>析构器和泄漏等整个话题可以追溯到大约 Rust 1.0，当时我们发现，实际上，我们的线程抽象在与循环引用计数的 box 结合使用时是不健全的。在此之前，我们还没有完全内化析构器是“选择退出的方法”。你可以阅读我当时写的<a href="https://smallcultfollowing.com/babysteps/blog/2015/04/29/on-reference-counting-and-leaks/" rel="noopener noreferrer">这篇博文</a>。当时，主要想法是某种 <code>?Leak</code> 约束，并且它与引用的概念相关（因此所有 <code>'static</code> 数据被假定为“可泄漏的”，因此你可以放入 <code>Rc</code>）。我……基本上认为当时我们做出了正确的选择。我认为生态系统中的大部分是可互操作的，<code>Rc</code> 不需要 <code>static</code> 约束是好的，当然我认为我们以最小的干扰转向 1.0 是好的。无论如何，我更喜欢这个设计，而不是当时讨论的那些设计，部分原因是它还解决了需要不同类型析构器和带多个参数的析构器等问题，而这些在当时我们没有考虑到。</p>
<h3 id="isnt-it-confusing-to-have-these-magic-traits-that-opt-out-from-default-bounds">拥有这些“选择退出”默认约束的“魔法” trait 不会令人困惑吗？</h3>
<p>我认为指定你<em>想要的约束</em>本质上比今天的 <code>?</code> 设计更好，不仅因为它更容易理解，还因为它允许我们向后兼容地以 <code>?</code> 设计不可能的方式添加中间 trait。</p>
<p>然而，我确实看到 <code>T: Move</code> 意味着 <code>T: Destruct</code> 不成立是微妙的。我想知道我们是否应该在这些 trait 上采用某种符号或约定，比如 <code>T: @Move</code> 或类似的东西。我不知道！需要考虑的事情。</p>
<div>
<hr>
<ol>
<li id="fn:1">
<p>那是一个很棒的会议。而且，有趣的是，这是我所有演讲中最喜欢的一个，但出于某种原因，我很少重用这些材料。我应该改变这一点。&nbsp;<a href="#fnref:1" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:2">
<p>学者区分“安全性”和“活性属性”，其中安全性意味着“坏事不发生”，而“活性”意味着“好事最终发生”。另一种说法是，Rust 的类型系统有助于许多安全属性，但在活性属性方面举步维艰。&nbsp;<a href="#fnref:2" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:3">
<p>呃，需要引用。我知道这是真的，但我找不到讨论这一点的相关 WebAssembly 问题。救命，互联网！&nbsp;<a href="#fnref:3" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:4">
<p>实际上，DMA 问题与作用域线程相同。如果你仔细想想，嵌入式设备写入内存基本上就像一个并行线程写入内存。&nbsp;<a href="#fnref:4" rel="noopener noreferrer">↩︎</a></p>
</li>
</ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 26151 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2025/10/21/move-destruct-leak/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2025/10/21/move-destruct-leak/</guid>
      <pubDate>Tue, 21 Oct 2025 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>自行车棚化 `Handle` 及其他后续思考</title>
      <category>ergonomic-rc</category>
      <description>[AI 摘要] 该文探讨了为 Rust 新 trait 寻找合适名称（如 Share）并优化克隆操作的人体工程学设计。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 该文探讨了为 Rust 新 trait 寻找合适名称（如 Share）并优化克隆操作的人体工程学设计。</div><p>对我提出的 <code>Handle</code> trait 提案，目前主要有两大类反馈。第一类是：该 trait 似乎有用，但无法涵盖所有需要便捷克隆的场景。第二类是：该名称不符合 Rust 的 trait 命名惯例——Rust 倾向于使用简短的动词而非名词。我的回应要点如下：（1）我同意，因此我认为应同时致力于让 <code>Clone</code> 和 <code>Handle</code> 都符合人体工程学；（2）我也同意，因此我认为需要另寻一个名称。目前我倾向于 <code>Share</code>，其次是 <code>Alias</code>。</p>
<h2 id="handle-doesnt-cover-everything">Handle 无法涵盖所有情况</h2>
<p>关于 <code>Handle</code> trait 的第一个顾虑是：虽然它为何时实现该 trait 提供了清晰的语义基础，但并未覆盖所有调用 <code>clone</code> 令人烦恼的场景。换句话说，如果我们选择使用 <code>Handle</code>，并使其创建过程非常符合人体工程学，但调用 <code>clone</code> 依然痛苦，那么当 <code>Handle</code> 并不适用时，人们可能仍会忍不住使用它。</p>
<p>在我们语言团队的一次设计会议上，TC 指出：对于许多应用而言，即使一次“昂贵的”克隆也并非真正大问题。例如，在编写命令行工具等程序时，我经常克隆字符串、字符串向量、哈希表和其他各种数据；我可以将它们放入 Rc 或 Arc，但我知道这其实无关紧要。</p>
<p>我的解决方案很简单：让我们提出一套能同时应用于 <code>Clone</code> 和 <code>Handle</code> 的方案。鉴于我认为需要一个提案，允许句柄既符合人体工程学又保持显式，那么将该解决方案扩展至包含克隆选项也并非难事。</p>
<p><a href="https://smallcultfollowing.com/babysteps/
/blog/2025/10/22/explicit-capture-clauses.html" rel="noopener noreferrer">显式捕获子句</a>的博文已符合此设计理念。我明确选择了一种设计，允许用户编写 <code>move(a.b.c.clone())</code> 或 <code>move(a.b.c.handle())</code>，因此它能同样好地（或同样不好地……）同时适用于这两个 trait。</p>
<h2 id="the-name-handle-doesnt-fit-the-rust-conventions">名称 <code>Handle</code> 不符合 Rust 惯例</h2>
<p>许多人指出 <code>Handle</code> 不符合 Rust 对此类 trait 的命名惯例，这些惯例旨在使用简短的动词。你可以将 <code>handle</code> 解释为动词，但其含义并非我们所指。这很合理。我原本喜欢 <code>Handle</code> 这个名称，因为它提供了一个我们可以用来谈论<em>句柄</em>的<em>名词</em>，但我同意该 trait 名称似乎并不合适。围绕可能的选项进行了大量自行车棚化讨论，但我想我最终回到了更倾向于 Jack Huey 的原始提案 <code>Share</code>（对应方法 <code>share</code>）。我认为 <code>Alias</code> 和 <code>alias</code> 是我的第二选择。两者都是简短且相对常用的动词。</p>
<p>我最初觉得 <code>Share</code> 有点过于通用，且容易让人联想到线程间共享——但我至少总是称 <code>&amp;T</code> 为<em>共享引用</em><sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup>，而 <code>&amp;T</code> 将实现 <code>Share</code>，所以一切似乎都配合得很好。感谢 Ariel Ben-Yehuda 推动我选择这个特定名称。</p>
<h2 id="coming-up-next">后续计划</h2>
<p>本系列中的一系列博文旨在概述此领域已进行的所有讨论。我的目标尚非撰写最终提案——我认为最终成果将是一系列 RFC。</p>
<p>我目前的想法是：我们应该添加 <code>Hand^H^H^H^H</code>，呃，<code>Share</code> trait。我也认为应该添加<a href="https://smallcultfollowing.com/babysteps/
/blog/2025/10/22/explicit-capture-clauses.html" rel="noopener noreferrer">显式捕获子句</a>。然而，虽然显式捕获子句显然“足够底层以用于内核”，但我并不认为它们“足够易用以用于 GUI”。下一篇文章将探讨另一个想法，我认为它可能让我们更接近最终的<a href="https://smallcultfollowing.com/babysteps/
/blog/2025/10/13/ergonomic-explicit-handles.html" rel="noopener noreferrer">符合人体工程学且显式</a>的目标。</p>
<div>
<hr>
<ol>
<li id="fn:1">
<p>许多人说<em>不可变引用</em>，但这并不准确：<code>&amp;Mutex</code> 并非不可变。我认为“共享引用”这个术语更好。&nbsp;<a href="#fnref:1" rel="noopener noreferrer">↩︎</a></p>
</li>
</ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 3019 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2025/11/05/bikeshedding-handle/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2025/11/05/bikeshedding-handle/</guid>
      <pubDate>Wed, 5 Nov 2025 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>但话说回来…或许Alias？</title>
      <category>ergonomic-rc</category>
      <description>[AI 摘要] 开发者考虑将某个概念命名为“Alias”而非“Share”，因其在Rust语境中兼具名词和动词的灵活性。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 开发者考虑将某个概念命名为“Alias”而非“Share”，因其在Rust语境中兼具名词和动词的灵活性。</div><p>嗯，当我重读我几分钟前刚发布的帖子时，我开始思考。也许正确的名称确实是<code>Alias</code>，而不是<code>Share</code>。理由很简单：alias既可以作为名词也可以作为动词。它正好符合“足够常见你知道它的意思，但又足够独特可以成为Rust里某个特定东西的术语”这个甜蜜点。就像我们说“传递一个<code>foo</code>的克隆”一样，我们可以谈论“传递一个指向<code>foo</code>的别名”或“一个<code>foo</code>的别名”。值得思考！我打算在以后的帖子中试试用<code>Alias</code>，看看感觉如何。</p><p><em>由 mimo-v2.5 模型翻译，花费 688 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2025/11/05/maybe-alias/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2025/11/05/maybe-alias/</guid>
      <pubDate>Wed, 5 Nov 2025 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>移动表达式</title>
      <category>ergonomic-rc</category>
      <description>[AI 摘要] 本文介绍 Rust 语言中名为&quot;移动表达式&quot;的新提案，旨在通过提供更精确的捕获控制来改进闭包的人体工程学和可读性。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 本文介绍 Rust 语言中名为"移动表达式"的新提案，旨在通过提供更精确的捕获控制来改进闭包的人体工程学和可读性。</div><p>本文探讨了<strong>人体工程学引用计数</strong>领域的另一项提案，我称之为<strong>移动表达式</strong>。在我看来，这是<a href="https://smallcultfollowing.com/babysteps/
/blog/2025/10/22/explicit-capture-clauses.html" rel="noopener noreferrer">显式捕获子句</a>的替代方案，它以更佳的人体工程学和可读性，实现了该设计中的许多（但并非<em>全部</em>）目标。</p>
<h2 id="tldr">太长不看版</h2>
<p>理念本身很简单：在闭包（或 future）中，我们增加书写<code>move($expr)</code>的选项。这是一个值表达式（"右值"），它会脱糖为一个临时值，该值被移入闭包。因此</p>
<div><pre><code><span><span><span>||</span><span> </span><span>something</span><span>(</span><span>&amp;</span><span>move</span><span>(</span><span>$expr</span><span>))</span><span>
</span></span></span></code></pre></div><p>大致等价于：</p>
<div><pre><code><span><span><span>{</span><span> 
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>tmp</span><span> </span><span>=</span><span> </span><span>$expr</span><span>;</span><span>
</span></span></span><span><span><span>    </span><span>||</span><span> </span><span>something</span><span>(</span><span>&amp;</span><span>{</span><span>tmp</span><span>})</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><h2 id="how-it-would-look-in-practice">实践中的效果</h2>
<p>让我们回到我们的一个常见示例——"Cloudflare 示例"，它最初来自<a href="https://dioxus.notion.site/Dioxus-Labs-High-level-Rust-5fe1f1c9c8334815ad488410d948f05e" rel="noopener noreferrer">Dioxus 团队的这篇精彩博文</a>。提醒一下，以下是代码<em>现在</em>的样子——注意那些用于处理捕获的<code>let _some_value = ...</code>行：</p>
<div><pre><code><span><span><span>// 任务：监听 dns 连接
</span></span></span><span><span><span>let</span><span> </span><span>_some_a</span><span> </span><span>=</span><span> </span><span>self</span><span>.</span><span>some_a</span><span>.</span><span>clone</span><span>();</span><span>
</span></span></span><span><span><span>let</span><span> </span><span>_some_b</span><span> </span><span>=</span><span> </span><span>self</span><span>.</span><span>some_b</span><span>.</span><span>clone</span><span>();</span><span>
</span></span></span><span><span><span>let</span><span> </span><span>_some_c</span><span> </span><span>=</span><span> </span><span>self</span><span>.</span><span>some_c</span><span>.</span><span>clone</span><span>();</span><span>
</span></span></span><span><span><span>tokio</span>::<span>task</span>::<span>spawn</span><span>(</span><span>async</span><span> </span><span>move</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>  	</span><span>do_something_else_with</span><span>(</span><span>_some_a</span><span>,</span><span> </span><span>_some_b</span><span>,</span><span> </span><span>_some_c</span><span>)</span><span>
</span></span></span><span><span><span>});</span><span>
</span></span></span></code></pre></div><p>在该提案下，它将大致如下所示：</p>
<div><pre><code><span><span><span>tokio</span>::<span>task</span>::<span>spawn</span><span>(</span><span>async</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>do_something_else_with</span><span>(</span><span>
</span></span></span><span><span><span>        </span><span>move</span><span>(</span><span>self</span><span>.</span><span>some_a</span><span>.</span><span>clone</span><span>()),</span><span>
</span></span></span><span><span><span>        </span><span>move</span><span>(</span><span>self</span><span>.</span><span>some_b</span><span>.</span><span>clone</span><span>()),</span><span>
</span></span></span><span><span><span>        </span><span>move</span><span>(</span><span>self</span><span>.</span><span>some_c</span><span>.</span><span>clone</span><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></pre></div><p>有时你需要多个克隆。例如，如果你想将某些内容移入一个<code>FnMut</code>闭包，该闭包随后会在每次调用时放弃一个副本，它可能看起来像这样</p>
<div><pre><code><span><span><span>data_source_iter</span><span>
</span></span></span><span><span><span>    </span><span>.</span><span>inspect</span><span>(</span><span>|</span><span>item</span><span>|</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>inspect_item</span><span>(</span><span>item</span><span>,</span><span> </span><span>move</span><span>(</span><span>tx</span><span>.</span><span>clone</span><span>()).</span><span>clone</span><span>())</span><span>
</span></span></span><span><span><span>        </span><span>//                      ----------  -------
</span></span></span><span><span><span>        </span><span>//                           |         |
</span></span></span><span><span><span>        </span><span>//                   将一个克隆移动     |
</span></span></span><span><span><span>        </span><span>//                   到闭包中          |
</span></span></span><span><span><span>        </span><span>//                                     |
</span></span></span><span><span><span>        </span><span>//                             每次迭代时克隆该克隆
</span></span></span><span><span><span>    </span><span>})</span><span>
</span></span></span><span><span><span>    </span><span>.</span><span>collect</span><span>();</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>// 一些后续使用 `tx` 的代码...
</span></span></span></code></pre></div><h2 id="credit-for-this-idea">致谢</h2>
<p>这个想法并非我首创。它已被多次提出。我记得第一次听说是在 RustConf 未安排会议期间，但我感觉它更早之前就已出现。最近的一次是由<a href="https://rust-lang.zulipchat.com/#narrow/channel/410673-t-lang.2Fmeetings/topic/Design.20meeting.202025-08-27.3A.20Ergonomic.20RC/near/555236763" rel="noopener noreferrer">Zachary Harrold 在 Zulip 上提出</a>的，他还创建了一个名为<a href="https://crates.io/crates/soupa" rel="noopener noreferrer">soupa</a>的原型。Zachary 的提案（以及我之前听说的更早提案）使用了<code>super</code>关键字。后来<a href="https://rust-lang.zulipchat.com/#narrow/channel/410673-t-lang.2Fmeetings/topic/Design.20meeting.202025-08-27.3A.20Ergonomic.20RC/near/555643180" rel="noopener noreferrer">@simulacrum 提议使用<code>move</code></a>，这对我来说是一个重大改进，因此我在此采用了这个版本。</p>
<h2 id="this-proposal-makes-closures-more-continuous">此提案使闭包更具"连续性"</h2>
<p>我喜欢这个提案的<code>move</code>变体的原因是，它使闭包更具"连续性"，并更清晰地揭示了其底层模型。有了这个设计，我会从使用移动表达式的闭包开始讲解，最后才教<code>move</code>闭包，将其作为一种便捷的默认方式：</p>
<blockquote>
<p>Rust 闭包以"最简方式"捕获你在闭包中使用的位置——因此<code>|| vec.len()</code>会捕获对<code>vec</code>的共享引用，<code>|| vec.push(22)</code>会捕获可变引用，而<code>|| drop(vec)</code>会获取向量的所有权。</p>
<p>你可以使用<code>move</code>表达式来精确控制捕获的内容：因此<code>|| move(vec).push(22)</code>会将<code>vector</code>移入闭包。当你想要完全显式时，一种常见模式是在闭包顶部列出所有捕获，如下所示：</p>
<div><pre><code><span><span><span>||</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>vec</span><span> </span><span>=</span><span> </span><span>move</span><span>(</span><span>input</span><span>.</span><span>vec</span><span>);</span><span> </span><span>// 完全获取 vec 的所有权
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>data</span><span> </span><span>=</span><span> </span><span>move</span><span>(</span><span>&amp;</span><span>cx</span><span>.</span><span>data</span><span>);</span><span> </span><span>// 获取对 data 的引用
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>output_tx</span><span> </span><span>=</span><span> </span><span>move</span><span>(</span><span>output_tx</span><span>);</span><span> </span><span>// 获取输出通道的所有权
</span></span></span><span><span><span>
</span></span></span><span><span><span>    </span><span>process</span><span>(</span><span>&amp;</span><span>vec</span><span>,</span><span> </span><span>&amp;</span><span>mut</span><span> </span><span>output_tx</span><span>,</span><span> </span><span>data</span><span>)</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>作为简写，你可以在闭包顶部写<code>move ||</code>，这将改变默认行为，使闭包获取所有捕获变量的所有权。你仍然可以使用<code>move</code>表达式进行混合搭配以获得更多控制。因此，前面的闭包可以更简洁地写成这样：</p>
<div><pre><code><span><span><span>move</span><span> </span><span>||</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>process</span><span>(</span><span>&amp;</span><span>input</span><span>.</span><span>vec</span><span>,</span><span> </span><span>&amp;</span><span>mut</span><span> </span><span>output_tx</span><span>,</span><span> </span><span>move</span><span>(</span><span>&amp;</span><span>cx</span><span>.</span><span>data</span><span>))</span><span>
</span></span></span><span><span><span>    </span><span>//       ---------       ---------       --------      
</span></span></span><span><span><span>    </span><span>//           |               |               |         
</span></span></span><span><span><span>    </span><span>//           |               |       闭包仍然
</span></span></span><span><span><span>    </span><span>//           |               |       捕获对 `&amp;cx.data` 的引用
</span></span></span><span><span><span>    </span><span>//           |               |                         
</span></span></span><span><span><span>    </span><span>//           |               |                         
</span></span></span><span><span><span>    </span><span>//           由于闭包上的 `move` 关键字，
</span></span></span><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></pre></div></blockquote>
<h2 id="this-proposal-makes-move-fit-in-for-me">此提案使<code>move</code>对我来说"更契合"</h2>
<p>有点讽刺的是我喜欢这个，因为它加强了 Rust 设计中我最近一直在抱怨的部分。在我之前关于<a href="https://smallcultfollowing.com/babysteps/
/blog/2025/10/22/explicit-capture-clauses.html" rel="noopener noreferrer">显式捕获子句</a>的文章中，我写道：</p>
<blockquote>
<p>老实说，我不喜欢<code>move</code>的选择，因为它太<em>操作化</em>了。我想，如果我能够回到过去，我会尝试围绕两个概念来重塑我们的闭包</p>
<ul>
<li><em>依附</em>闭包（我们现在称之为<code>||</code>）将<em>总是</em>绑定到外部栈帧。即使它们没有捕获任何内容，它们也始终具有生命周期。</li>
<li><em>分离</em>闭包（我们现在称之为<code>move ||</code>）将按值捕获，就像今天的<code>move</code>一样。</li>
</ul>
<p>我认为这将有助于建立"如果你要将闭包从当前栈帧返回，使用<code>detach ||</code>，否则使用<code>||</code>"的直觉。</p>
</blockquote>
<p>移动表达式，我认为，正朝着相反的方向前进。它们没有谈论依附和分离，而是将我们带向更统一的闭包概念，在那里你没有"引用闭包"和"移动闭包"——你只是拥有有时捕获移动的闭包，而"移动"闭包只是在所有地方都使用<code>move</code>表达式的简写。这实际上正是闭包在编译器底层的工作方式，我认为这非常优雅。</p>
<h2 id="why-not-suffix">为什么不用后缀？</h2>
<p>一个问题是移动表达式应该是<em>前缀</em>还是<em>后缀</em>运算符。例如</p>
<div><pre><code><span><span><span>||</span><span> </span><span>something</span><span>(</span><span>&amp;</span><span>$expr</span><span>.</span><span>move</span><span>)</span><span>
</span></span></span></code></pre></div><p>而不是<code>&amp;move($expr)</code>。</p>
<p>我的感觉是，它不适合后缀运算符，因为它不仅仅处理表达式的最终值然后对其做些什么，它实际上影响整个表达式的求值时间。考虑这个例子：</p>
<div><pre><code><span><span><span>||</span><span> </span><span>process</span><span>(</span><span>foo</span><span>(</span><span>bar</span><span>()).</span><span>move</span><span>)</span><span>
</span></span></span></code></pre></div><p><code>bar()</code> 何时被调用？如果你仔细想想，它必须在闭包创建时调用，但这并不非常"明显"。</p>
<p>当我们考虑 <code>.unsafe</code> 运算符时，得出了类似的结论。我认为有一条经验法则：那些划定代码"范围"的东西应该是前缀——尽管我怀疑 <code>unsafe(expr)</code> 可能实际上不错，而不仅仅是 <code>unsafe { expr }</code>。</p>
<p><em>编辑：</em>我在后记中添加了这部分内容以回应问题。</p>
<h2 id="conclusion">结论</h2>
<p>我将在此结束本文。老实说，这种设计真正拥有的优势，超越其他一切，是其<em>简洁性</em>以及它<em>如何泛化 Rust 现有设计</em>的方式。我喜欢这一点。对我来说，它加入了"是的，我们显然应该那样做"的拼图集合：</p>
<ul>
<li>添加一个 <code>Share</code> trait（我已回到更喜欢 <code>share</code> 这个名称 🙂）</li>
<li>添加移动表达式</li>
</ul>
<p>这两者似乎都是坚实的进步。我尚未被说服它们能达到我在<a href="https://smallcultfollowing.com/babysteps/
/blog/2025/10/13/ergonomic-explicit-handles/" rel="noopener noreferrer">早期文章</a>中阐述的目标：</p>
<blockquote>
<p>"底层到足以用于内核，易用到足以用于 GUI"</p>
</blockquote>
<p>但它们正朝着正确的方向前进。</p><p><em>由 mimo-v2.5 模型翻译，花费 12660 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2025/11/21/move-expressions/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2025/11/21/move-expressions/</guid>
      <pubDate>Fri, 21 Nov 2025 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>与达达同乐</title>
      <category>dada</category>
      <description>[AI 摘要] 本文介绍了作者的实验性编程语言“达达”的发展历程和主要特性，包括其作为 Rust 精简版、原生的 WebAssembly 目标以及更精巧的借用检查器。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 本文介绍了作者的实验性编程语言“达达”的发展历程和主要特性，包括其作为 Rust 精简版、原生的 WebAssembly 目标以及更精巧的借用检查器。</div><img src="https://smallcultfollowing.com/babysteps/assets/2026-fun-with-dada/dada-logo.svg" width="20%">
<p>早在 2021 年，我开始尝试一门我称之为“达达”的新编程语言。自那以后，我一直都在捣鼓它，而我刚刚才意识到（天哪！）我居然从未为此写过一篇博文！我想我该修正这一点。这篇文章将介绍当前达达语言的一些基本概念。</p>
<p>在你产生任何想法之前，达达目前还不适合使用。事实上，编译器甚至都无法正常工作，因为我总是在它完全完成之前就修改这门语言。老实说，达达对我来说更像一个“减压阀”<sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup>——摆弄一门不用操心向后兼容性、RFC 或其他任何事情的编程语言很有趣。</p>
<p>话虽如此，达达一直是创意的沃土，我认为这些想法可能适用于 Rust。而且不仅仅是语言设计：摆弄编译器也促成了新版 <code>salsa</code> 的设计<sup id="fnref:2"><a href="#fn:2" rel="noopener noreferrer">2</a></sup>，它现在被 rust-analyzer 和 <a href="https://github.com/astral-sh/ty" rel="noopener noreferrer">Astral 的 ty</a> 使用。所以我真的很想把这些想法分享出去！</p>
<h2 id="i-took-a-break-but-im-back-baby">我休息了一阵，但现在回归了，宝贝！</h2>
<p>大约一年前<sup id="fnref:3"><a href="#fn:3" rel="noopener noreferrer">3</a></sup>，我停止了折腾达达，但过去几天我又开始重新工作。然后我意识到，嘿，现在正是开始写博客的绝佳时机！毕竟，无论如何我都要重新回忆起之前的进展，而写作总是厘清细节的最佳方式。</p>
<h2 id="dada-started-as-a-gradual-programming-experiment-but-no-longer">达达始于渐进式编程实验，但已不再是</h2>
<p>达达经历了许多阶段。早期的目标是构建一门渐进类型化的编程语言，我认为它会更容易让人们学习。</p>
<p>这个想法是，你可以完全不写任何类型就直接开始编写并执行程序。有一个交互式演练场，可以让你在进行过程中逐步查看和可视化“借用检查器”状态（达达称之为权限）。我的希望是，人们会发现这比直接使用类型检查器更容易上手。</p>
<p>我实现了这个功能，它实际上非常酷。<a href="https://www.youtube.com/watch?v=tdg03gEbyS8" rel="noopener noreferrer">我在 2022 年的编程语言指导研讨会上就此做了演讲</a>，不过快速浏览一下视频，我似乎并没有真正演示权限建模。太可惜了。</p>
<p>与此同时，我发现自己并不完全信服渐进式方法。我希望的是，当你在没有类型注解的情况下执行程序时，你能在违反借用规则的地方获得错误。这意味着程序必须跟踪大量额外数据，有点像 miri 的做法，而这实际上只适合作为教学工具。我仍然想探索这一点，但它也为语言设计增加了很多复杂性，而这点可能只在开发者学习旅程的早期有用<sup id="fnref:4"><a href="#fn:4" rel="noopener noreferrer">4</a></sup>。</p>
<p>因此，我决定从头再来，这次只专注于达达的静态类型检查部分。</p>
<h2 id="dada-is-like-a-streamlined-rust">达达就像一个精简的 Rust</h2>
<p>今天的达达就像 Rust，但经过了<em>精简</em>。目标是让达达拥有与 Rust 相同的基本“所有权导向”的<em>感觉</em>，但需要处理的选择和琐碎细节要少得多<sup id="fnref:5"><a href="#fn:5" rel="noopener noreferrer">5</a></sup>。</p>
<p>Rust 经常有语义等价但表示不同的类型。考虑 <code>&amp;Option&lt;String&gt;</code> 和 <code>Option&lt;&amp;String&gt;</code>：它们在功能上是等价的，但 Rust 当然会要求你仔细区分它们。在达达中，它们是同一个类型。达达也让 <code>&amp;Vec&lt;String&gt;</code>、<code>&amp;Vec&lt;&amp;String&gt;</code>、<code>&amp;[String]</code>、<code>&amp;[&amp;str]</code> 以及许多其他变体都成为同一类型。在你问之前，它不是通过堆分配所有东西或使用垃圾收集器来实现的。</p>
<p>简而言之，达达旨在成为<strong>“让你永远不必调用 <code>as_ref()</code> 的 Rust”</strong>。</p>
<h2 id="dada-has-a-fancier-borrow-checker">达达拥有一个更精巧的借用检查器</h2>
<p>达达还有一个更精巧的借用检查器，它已经展示了<a href="https://smallcultfollowing.com/babysteps/blog/2024/06/02/the-borrow-checker-within/" rel="noopener noreferrer">内部借用检查器</a>的许多特性，尽管它还没有视图类型。达达的借用检查器支持<a href="https://smallcultfollowing.com/babysteps/blog/2024/06/02/the-borrow-checker-within/#step-4-internal-references" rel="noopener noreferrer">内部借用</a>（例如，你可以创建一个结构体，其字段从其他字段借用），并且支持<a href="https://smallcultfollowing.com/babysteps/blog/2024/03/04/borrow-checking-without-lifetimes/" rel="noopener noreferrer">无生命周期的借用检查</a>。这些东西有很多可以引入 Rust，尽管我在达达中调整了一些东西，使得某些方面更容易实现。</p>
<h2 id="dada-targets-webassembly-natively">达达原生以 WebAssembly 为目标</h2>
<p>在重新聚焦达达的过程中，我决定专注于构建 WebAssembly 组件。最初，我认为以 WebAssembly 为目标会非常方便：</p>
<ul>
<li>WebAssembly 就像一个非常简单干净的汇编语言，所以编写编译器后端很容易。</li>
<li>WebAssembly 组件被明确设计用来桥接不同语言，所以它们为你解决了 FFI 问题。</li>
<li>借助 WASI，你甚至能获得一个功能齐全的标准库，包含诸如“获取网页”之类的高级功能。因此你可以立即构建有用的东西。</li>
</ul>
<h2 id="webassembly-and-on-demand-compilation--compile-time-reflection-almost-for-free">WebAssembly 与按需编译 = 几乎免费获得编译时反射</h2>
<p>但我逐渐意识到，以 WebAssembly 为目标还有另一个优势：<strong>它使得编译时反射几乎变得微不足道</strong>。达达编译器采用纯按需方式构建。这意味着我们可以将一个函数一直编译成 WebAssembly 字节码，而让 crate 的其余部分保持不变。</p>
<p>一旦我们有了 WebAssembly 字节码，我们就可以在编译器内部运行它！借助 wasmtime，我们有一个高质量的 JIT，运行速度非常快。代码甚至还是沙箱化的！</p>
<p>因此，我们可以有一个函数，在执行期间编译并运行它，并用它来生成其他代码，供编译过程的后续步骤使用。换句话说，我们基本上免费获得了类似 miri 或 Zig 的 comptime 的东西。哇哦。</p>
<h2 id="wish-you-could-try-it-me-too">希望你能试试它？我也想！</h2>
<p>天哪，写这篇博文让我自己也兴奋起来想玩达达了。可惜它实际上还不能用。哈哈！但我计划继续推进编译器，尽快让它达到可以现场演示的程度。具体要多久还很难说。</p>
<p>同时，为了帮助我重新了解它的工作原理，我打算尝试写一系列关于类型系统、借用检查器和编译器架构的博文，我认为这些都很有趣。</p>
<div>
<hr>
<ol>
<li id="fn:1">
<p>是的，我通过设计新的编程语言来放松。不是每个人都这样吗？&nbsp;<a href="#fnref:1" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:2">
<p>现在回想起来，设计新版本的 <a href="https://salsa-rs.github.io/salsa/" rel="noopener noreferrer"><code>salsa</code></a> 以便我能以想要的方式编写达达编译器，真的是一次史诗级的挖毛衣（意指解决无关紧要的琐事）行动。&nbsp;<a href="#fnref:2" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:3">
<p>当我开始对大语言模型产生兴趣后，我失去了动力。坦率地说，我觉得我必须足够了解它们，才能明白设计一门编程语言是否是在“打上一场战争”。在接触了很多大语言模型之后，我确实觉得它们让编程语言的选择变得不那么重要了。但我也认为它们确实比人类更受益于高级抽象，所以我喜欢认为达达可能仍然有用。此外，它很有趣。&nbsp;<a href="#fnref:3" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:4">
<p>而且，有了大语言模型，那段学习期比以往任何时候都短。&nbsp;<a href="#fnref:4" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:5">
<p>当然，这也使得达达不够灵活。我怀疑像 Rust for Linux 这样的项目能在达达上工作。&nbsp;<a href="#fnref:5" rel="noopener noreferrer">↩︎</a></p>
</li>
</ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 5576 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2026/02/08/fun-with-dada/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2026/02/08/fun-with-dada/</guid>
      <pubDate>Sun, 8 Feb 2026 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>你好，达达！</title>
      <category>dada</category>
      <description>[AI 摘要] 本文是达达编程语言的入门教程，介绍了其简化编程、默认便利特性（如字符串插值）和基础语法。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 本文是达达编程语言的入门教程，介绍了其简化编程、默认便利特性（如字符串插值）和基础语法。</div><img src="https://smallcultfollowing.com/babysteps/
/assets/2026-fun-with-dada/dada-logo.svg" width="20%">
<p>继我的<a href="https://smallcultfollowing.com/babysteps/blog/2026/02/08/fun-with-dada/" rel="noopener noreferrer">“达达之乐”</a>一文之后，本篇将开始教授达达语言。我会保持每篇文章简短——基本上就是趁喝晨间咖啡的时间能写多少算多少。<sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup></p>
<h2 id="you-have-the-right-to-write-code">你有权编写代码</h2>
<p>这是一个最基础的达达程序</p>
<pre><code>println("Hello, Dada!")</code></pre>
<p>我想大家都能猜出它的功能。但即便在这个简单的程序中，也有些值得注意之处：</p>
<p><strong>“你有权编写代码。如果你没有显式编写 <code>main</code> 函数，系统会为你提供一个。”</strong> 我很早就做了这个改动，允许用户省略 <code>main</code> 函数，结果很惊讶它对语言<em>轻盈感</em>的提升如此明显。改动虽小，收效却大。</p>
<h2 id="convenient-is-the-default">便利即默认</h2>
<p>这是另一个达达程序</p>
<pre><code>let name = "Dada"
println("Hello, {name}!")</code></pre>
<p>不出所料，这个程序与上一个功能相同。</p>
<p><strong>“便利即默认。”</strong> 字符串默认支持插值（即 <code>{name}</code>）。实际上，这还不是全部，它们还能非常方便地跨行书写。下面这个程序与我们见过的前几个效果一致：</p>
<pre><code>let name = "Dada"
println("
    Hello, {name}!
")</code></pre>
<p>当引号 <code>"</code> 后直接跟换行时，开头和结尾的换行符会被去除，后续行的“空白前缀”也会被移除。内部的换行符会被保留，因此像下面这样的代码：</p>
<pre><code>let name = "Dada"
println("
    Hello, {name}!
    
    How are you doing?
")</code></pre>
<p>会输出：</p>
<div><pre><code><span><span>Hello, Dada!
</span></span><span><span>
</span></span><span><span>How are you doing?
</span></span></code></pre></div><h2 id="just-one-familiar-string">只有一种熟悉的 <code>String</code></h2>
<p>当然，你也可以显式标注 <code>name</code> 变量的类型：</p>
<pre><code>let name: String = "Dada"
println("Hello, {name}!")</code></pre>
<p>你会发现类型是 <code>String</code>。这本身并不特别，除非你习惯了 Rust，那里的类型会是 <code>&amp;'static str</code>。这无疑是 Rust 新手的常见绊脚石，但更重要的是，我发现它是个大<em>麻烦</em>——我讨厌在任何需要混合常量字符串和动态构造字符串的地方，都得写 <code>"Foo".to_string()</code> 或 <code>format!("Foo")</code>。</p>
<p>与大多数现代语言类似，达达中的字符串是不可变的。因此你可以创建并复制它们：</p>
<pre><code>let name: String = "Dada"
let greeting: String = "Hello, {name}"
let name2: String = name</code></pre>
<h2 id="next-up-mutation-permissions">接下来：变更与权限</h2>
<p>好了，这里我们真的只是蜻蜓点水！这只是达达的“友好外衣”，它看起来和感觉上与千百万其他语言相似。下次我将开始介绍权限系统和变更机制，那里会更有趣一些。</p>
<div>
<hr>
<ol>
<li id="fn:1">
<p>我的习惯是大约凌晨5点起床，把一天的第一个小时用于“有趣的副项目”。但在过去的N个月里，我实际上一直在处理 Rust 相关事务，比如 <a href="https://symposium.dev/" rel="noopener noreferrer">symposium.dev</a> 和<a href="https://rust-lang.github.io/rust-project-goals/2026/" rel="noopener noreferrer">筹备2026年 Rust 项目目标</a>。这两件事都非常投入，但全是 Rust 没有玩乐会让尼科变成一个沉闷的男孩。也会变成一个脾气暴躁的男孩。&nbsp;<a href="#fnref:1" rel="noopener noreferrer">↩︎</a></p>
</li>
</ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 3226 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2026/02/09/hello-dada/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2026/02/09/hello-dada/</guid>
      <pubDate>Mon, 9 Feb 2026 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Dada：移动与变更</title>
      <category>dada</category>
      <description>[AI 摘要] 本文介绍了Dada语言中移动和变更的语法和机制，并与Rust进行了对比。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 本文介绍了Dada语言中移动和变更的语法和机制，并与Rust进行了对比。</div><p>让我们继续学习Dada。在我的<a href="https://smallcultfollowing.com/babysteps/blog/2026/02/09/hello-dada/" rel="noopener noreferrer">上一篇文章</a>中，我介绍了一些字符串操作。现在让我们开始讨论权限。这是Dada开始更像Rust的地方。</p>
<h2 id="class-struggle">类斗争</h2>
<p>Dada中的<strong>类</strong>是我们声明新类型的基本方式之一（还有枚举，我们稍后会讲到）。</p>
<p>最方便的声明类的方法是将字段放在括号中。这同时隐式声明了一个构造函数：</p>
<pre><code>class Point(x: u32, y: u32) {}</code></pre>
<p>这实际上是更Rust风格形式的语法糖：</p>
<pre><code>class Point {
    x: u32
    y: u32
    fn new() -&gt; Point {
        Point { x, y }
    }
}</code></pre>
<p>你可以通过调用构造函数来创建类的实例：</p>
<pre><code>let p = Point(22, 44) // Point.new(22, 44)的语法糖</code></pre>
<h2 id="mutating-fields">变更字段</h2>
<p>我可以如你所预期地变更p的字段：</p>
<pre><code>p.x += 1
p.x = p.y</code></pre>
<h2 id="read-by-default">默认只读</h2>
<p>在Dada中，声明参数时的默认是获得只读访问权限：</p>
<pre><code>fn print_point(p: Point) {
    print("The point is {p.x}, {p.y}")
}

let p = Point(22, 44)
print_point(p)</code></pre>
<p>如果你尝试变更参数的字段，那会得到一个错误：</p>
<pre><code>fn print_point(p: Point) {
    p.x += 1 # &lt;-- 错误！
}</code></pre>
<h2 id="use--to-mutate">使用<code>!</code>进行变更</h2>
<p>如果你用<code>!</code>声明一个参数，那么它就变成调用者传入的类实例的可变引用：</p>
<pre><code>fn translate_point(point!: Point, x: u32, y: u32) {
    point.x += x
    point.y += y
}</code></pre>
<p>在Rust中，这类似于<code>point: &amp;mut Point</code>。当你调用<code>translate_point</code>时，你也放一个<code>!</code>来表示你<em>传递</em>一个可变引用：</p>
<pre><code>let p = Point(22, 44)     # 创建点
print_point(p)            # 打印 22, 44
translate_point(p!, 2, 2) # 变更点
print_point(p)            # 打印 24, 46 </code></pre>
<p>如你所见，当<code>translate_point</code>修改<code>p.x</code>时，它在原地改变了<code>p</code>。</p>
<h2 id="moves-are-explicit">移动是显式的</h2>
<p>如果你熟悉Rust，最后一个例子可能有点令人惊讶。在Rust中，像<code>print_point(p)</code>这样的调用会<em>移动</em><code>p</code>，交出所有权。之后尝试使用它会得到错误。这是因为Dada的默认是提供只读引用，类似于Rust中的<code>&amp;x</code>（这给出了正确的<em>直觉</em>，但也具有误导性；我们将在未来的一篇文章中看到Dada中的<em>引用</em>与Rust在一个非常重要的方面有所不同）。</p>
<p>如果你有一个函数需要其参数的所有权，你用<code>given</code>来声明：</p>
<pre><code>fn take_point(p: given Point) {
    // ...
}</code></pre>
<p>而在调用者方面，你用<code>.give</code>调用这样的函数：</p>
<pre><code>let p = Point(22, 44)
take_point(p.give)
take_point(p.give) # &lt;-- 错误！不能给两次。</code></pre>
<h2 id="comparing-with-rust">与Rust比较</h2>
<p>比较一些Rust和Dada代码并排很有趣：</p>
<table>
  <thead>
      <tr>
          <th>Rust</th>
          <th>Dada</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>vec.len()</code></td>
          <td><code>vec.len()</code></td>
      </tr>
      <tr>
          <td><code>map.get(&amp;key)</code></td>
          <td><code>map.get(key)</code></td>
      </tr>
      <tr>
          <td><code>vec.push(element)</code></td>
          <td><code>vec!.push(element.give)</code></td>
      </tr>
      <tr>
          <td><code>vec.append(&amp;mut other)</code></td>
          <td><code>vec!.append(other!)</code></td>
      </tr>
      <tr>
          <td><code>message.send_to(&amp;channel)</code></td>
          <td><code>message.give.send_to(channel)</code></td>
      </tr>
  </tbody>
</table>
<h2 id="design-rationale-and-objectives">设计原理与目标</h2>
<h3 id="convenient-is-the-default">便捷是默认的</h3>
<p>最便捷的事物是最短和最常见的。因此我们将读取作为默认。</p>
<h3 id="everything-is-explicit-but-unobtrusive">一切显式但不突兀</h3>
<p>Rust中的<code>.</code>运算符根据所调用的方法可以做各种事情。它可能变更、移动、创建临时对象等。在Dada中，这些事情都在调用点可见——但它们不突兀。</p>
<p>这实际上源于Dada的“渐进式编程”时代——毕竟，如果你在方法上没有类型注解，那么你就无法决定<code>foo.bar()</code>应该共享借用还是可变借用<code>foo</code>。所以我们需要一种在调用点显式可见的表示法。</p>
<h3 id="postfix-operators-play-more-nicely-with-others">后缀运算符与其他运算符配合更好</h3>
<p>Dada努力避免像<code>&amp;mut</code>这样的前缀运算符，因为它们与<code>.</code>表示法配合不佳。</p><p><em>由 mimo-v2.5 模型翻译，花费 5881 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2026/02/10/dada-moves-and-mutation/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2026/02/10/dada-moves-and-mutation/</guid>
      <pubDate>Tue, 10 Feb 2026 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>达达中的共享</title>
      <category>dada</category>
      <description>[AI 摘要] 该文章解释了达达语言如何通过内置的共享机制和权限传播，在没有垃圾回收的情况下实现类似GC的可组合体验，解决了Rust中常见的阻抗不匹配问题。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 该文章解释了达达语言如何通过内置的共享机制和权限传播，在没有垃圾回收的情况下实现类似GC的可组合体验，解决了Rust中常见的阻抗不匹配问题。</div>好的，让我们来谈谈<em>共享</em>。这是达达博客文章中第一篇内容开始与Rust产生深刻分歧的地方，我认为也是我们第一次开始看到达达做事方式的一些真正优势（以及我为实现这些优势所做的一些权衡）的地方。
<h2 id="we-are-shooting-for-a-gc-like-experience-without-gc">我们的目标是在没有GC的情况下获得类似GC的体验</h2>
<p>让我们从目标开始：早些时候，我说达达就像“你永远不必输入<code>as_ref</code>的Rust”。但我真正的意思是，我想要一种<em>类似GC的体验</em>——但没有GC。</p>
<h2 id="we-are-shooting-for-a-composable-experience">我们的目标是获得一种“可组合”的体验</h2>
<p>我也经常使用“可组合”一词来描述我追求的达达体验。<em>可组合</em>意味着你可以将不同的事物组合在一起以实现新的功能。</p>
<p>显然，Rust有许多可组合的模式——例如<code>Iterator</code> API。但我发现的是，Rust代码通常非常脆弱：在如何声明数据结构方面有许多选择，而你所做的选择将决定这些数据结构如何被使用。</p>
<h2 id="running-example-character">贯穿示例：<code>Character</code></h2>
<h3 id="defining-the-character-type">定义<code>Character</code>类型</h3>
<p>让我们创建一个可以在整篇文章中作为示例的类型：<code>Character</code>。在Rust中，我们可能会这样定义一个<code>Character</code>：</p>
<div><pre><code><span><span><span>#[derive(Default)]</span><span>
</span></span></span><span><span><span>struct</span> <span>Character</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>name</span>: <span>String</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>class</span>: <span>String</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>hp</span>: <span>u32</span><span>,</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><h3 id="creating-and-arcing-the-character">创建并Arc包裹<code>Character</code></h3>
<p>现在，假设由于某种原因，我们要以编程方式构建一个角色：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>mut</span><span> </span><span>ch</span><span> </span><span>=</span><span> </span><span>Character</span>::<span>default</span><span>();</span><span>
</span></span></span><span><span><span>ch</span><span>.</span><span>name</span><span>.</span><span>push_str</span><span>(</span><span>"Ferris"</span><span>);</span><span>
</span></span></span><span><span><span>ch</span><span>.</span><span>class</span><span>.</span><span>push_str</span><span>(</span><span>"Rustacean"</span><span>);</span><span>
</span></span></span><span><span><span>ch</span><span>.</span><span>hp</span><span> </span><span>=</span><span> </span><span>44</span><span>;</span><span>
</span></span></span></code></pre></div><p>到目前为止，一切都很好。现在假设我想共享同一个<code>Character</code>结构体，以便可以从许多地方引用它而无需深拷贝。为此，我将把它放入一个<code>Arc</code>中：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>mut</span><span> </span><span>ch</span><span> </span><span>=</span><span> </span><span>Character</span>::<span>default</span><span>();</span><span>
</span></span></span><span><span><span>ch</span><span>.</span><span>name</span><span>.</span><span>push_str</span><span>(</span><span>"Ferris"</span><span>);</span><span>
</span></span></span><span><span><span>// ...
</span></span></span><span><span><span>let</span><span> </span><span>ch1</span><span> </span><span>=</span><span> </span><span>Arc</span>::<span>new</span><span>(</span><span>ch</span><span>);</span><span>
</span></span></span><span><span><span>let</span><span> </span><span>ch2</span><span> </span><span>=</span><span> </span><span>ch1</span><span>.</span><span>clone</span><span>();</span><span>
</span></span></span></code></pre></div><p>好的，很酷！现在我有了一个可以轻松共享的<code>Character</code>。这很好。</p>
<h3 id="rust-is-composable-here-which-is-cool-we-like-that">Rust在这里是可组合的，这很酷，我们喜欢这样</h3>
<p>旁注，但这是Rust<em>可组合</em>的一个例子：我们以一种完全拥有的方式定义了<code>Character</code>一次，并且我们能够可变地使用它（随着时间的推移以命令式方式构建它），然后能够“冻结”它并获得一个只读的、共享的<code>Character</code>副本。这让我们既获得了命令式编程语言的优势（易于数据构造和操作），又获得了函数式语言的优势（不可变性可以防止当事物从许多不相交的地方被引用时出现错误）。很好！</p>
<h3 id="creating-and-arcing-the-character-1">创建并Arc包裹<code>Character</code></h3>
<p><em>现在</em>，假设我有其他一些独立编写的代码，<em>仅仅</em>需要存储角色的<em>名字</em>。该代码最终将名字复制到许多不同的地方。因此，就像我们使用<code>Arc</code>让我们能够从多个地方廉价地引用单个角色一样，它使用<code>Arc</code>以便可以从多个地方廉价地引用角色的<em>名字</em>：</p>
<div><pre><code><span><span><span>struct</span> <span>CharacterSheetWidget</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>// 使用 `Arc&lt;String&gt;` 而不是 `String` 因为我们最终
</span></span></span><span><span><span>    </span><span>// 会将其复制到不同的地方，而我们不想每次都深度克隆
</span></span></span><span><span><span>    </span><span>// 该字符串。
</span></span></span><span><span><span>    </span><span>name</span>: <span>Arc</span><span>&lt;</span><span>String</span><span>&gt;</span><span>,</span><span>
</span></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></pre></div><p>好的。现在问题来了。我想从我们的共享角色创建一个角色卡控件：</p>
<div><pre><code><span><span><span>fn</span> <span>create_character_sheet_widget</span><span>(</span><span>ch</span>: <span>Arc</span><span>&lt;</span><span>Character</span><span>&gt;</span><span>)</span><span> </span>-&gt; <span>CharacterSheetWidget</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>CharacterSheetWidget</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>// FIXME: 咦，我该如何弥合这个鸿沟？
</span></span></span><span><span><span>        </span><span>// 我想我必须这样做。
</span></span></span><span><span><span>        </span><span>name</span>: <span>Arc</span>::<span>new</span><span>(</span><span>ch</span><span>.</span><span>name</span><span>.</span><span>clone</span><span>()),</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><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></pre></div><p>哎，这真令人沮丧！我<em>想</em>做的是写<code>name: ch.name.clone()</code>或类似的东西（实际上我可能<em>想</em>直接写<code>ch.name</code>，但不管怎样），然后得到一个<code>Arc&lt;String&gt;</code>。但我做不到。相反，我必须深度克隆字符串<em>并且</em>分配一个<em>新的</em><code>Arc</code>。当然，任何后续的克隆都会很便宜。但这不太好。</p>
<h3 id="rust-often-gives-rise-to-these-kind-of-impedance-mismatches">Rust经常导致这类“阻抗不匹配”</h3>
<p>我经常在Rust中发现这类模式：一段代码和另一段代码之间存在某种“阻抗不匹配”。<em>解决方案</em>各不相同，但通常类似：</p>
<ul>
<li><em>克隆一些数据</em>——反正它也不大，管它呢（这里就是这种情况）。</li>
<li><em>重构一段代码</em>——例如，修改<code>Character</code>类以存储<code>Arc&lt;String&gt;</code>。当然，这会产生连锁反应，例如，我们不能再写<code>ch.name.push_str(...)</code>了，而必须使用<code>Arc::get_mut</code>之类的。</li>
<li><em>调用一些烦人的辅助函数</em>——例如，编写<code>opt.as_ref()</code>将<code>&amp;Option&lt;String&gt;</code>转换为<code>Option&lt;&amp;String&gt;</code>，或编写<code>&amp;**r</code>将<code>&amp;Arc&lt;String&gt;</code>转换为<code>&amp;str</code>。</li>
</ul>
<p>达达的目标就是我们不需要这类事情。</p>
<h2 id="sharing-is-how-dada-copies">共享是达达的复制方式</h2>
<p>那么让我们来看看同一个<code>Character</code>示例在达达中会如何实现。我们将首先定义<code>Character</code>类：</p>
<pre><code>class Character(
    name: String,
    klass: String,  # 哎呀，class关键字的危险！
    hp: u32,
)</code></pre>
<p>就像在Rust中一样，我们可以创建角色，然后对其进行修改：</p>
<pre><code>class Character(name: String, klass: String, hp: u32)

let ch: given Character = Character("", "", 22)
      # ----- 记住，“given”权限
      #       意味着 `ch` 是完全拥有的
ch.name!.push("Tzara")
ch.klass!.push("Dadaist")
   #    - 而 `!` 表示可变性</code></pre>
<h2 id="the-share-operator-creates-a-shared-object"><code>.share</code> 运算符创建一个 <code>shared</code> 对象</h2>
<p>好的。现在，我想共享这个角色，以便可以从许多地方引用它。在Rust中，我们创建了一个<code>Arc</code>，但在达达中，共享是“内置的”。我们使用<code>.share</code>运算符，它将把<code>given Character</code>（即完全拥有的角色）转换为<code>shared Character</code>：</p>
<pre><code>class Character(name: String, klass: String, hp: u32)

let ch = Character("", "", 22)
ch!.push("Tzara")
ch!.push("Dadaist")

let ch1: shared Character = ch.share
      #  ------                -----
      # `share` 运算符消费 `ch`
      # 并返回同一个对象，但现在具有
      # *共享* 权限。</code></pre>
<h2 id="shared-objects-can-be-copied-freely"><code>shared</code> 对象可以自由复制</h2>
<p>现在我们有了一个<code>shared</code>角色，我们可以复制它了：</p>
<pre><code>class Character(name: String, klass: String, hp: u32)

# 创建一个共享角色作为开始
let ch1 = Character("Tzara", "Dadaist", 22).share
    #                                       -----

# 创建另一个共享角色
let ch2 = ch1</code></pre>
<h2 id="sharing-propagates-from-owner-to-field">共享从所有者传播到字段</h2>
<p>当你有一个共享对象并访问其字段时，你得到的是<strong>该字段的共享（浅）副本</strong>：</p>
<pre><code>class Character(...)

# 创建一个 `shared Character`
let ch: shared Character = Character("Tristan Tzara", "Dadaist", 22).share
      # ------                                                       -----

# 提取 `name` 字段会得到一个 `shared String`
let name: shared String = ch1.name
        # ------</code></pre>
<h2 id="propagation-using-a-vec">使用<code>Vec</code>进行传播</h2>
<p>为了突出这一点多么酷和方便，想象一下我有一个<code>Vec[String]</code>，我用<code>.share</code>共享它：</p>
<pre><code>let v: shared Vec[String] = ["Hello", "Dada"].share</code></pre>
<p>然后我用<code>v.share</code>共享它。我得到的是一个<code>shared Vec[String]</code>。当我访问它的元素时，我得到的是一个<code>shared String</code>：</p>
<pre><code>let v = ["Hello", "Dada"].share
let s: shared String = v[0]</code></pre>
<p>这就像在Rust中可以取一个<code>Arc&lt;Vec&lt;String&gt;&gt;</code>并从中得到一个<code>Arc&lt;String&gt;</code>一样。</p>
<h2 id="how-sharing-is-implemented">共享是如何实现的</h2>
<p>那么共享是如何实现的呢？答案在于一个并非完全显而易见的内存布局。要了解它的工作原理，让我们看看<code>Character</code>在内存中是如何布局的：</p>
<pre><code># 我们之前看到的 Character 类型。
class Character(name: String, klass: String, hp: u32)

# String 类型可能类似这样。
class String {
    buffer: Pointer[char]
    initialized: usize
    length: usize
}</code></pre>
<p>这里的<code>Pointer</code>是一个内置类型，它是达达不安全代码系统的基础。<sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup></p>
<h3 id="layout-of-a-given-character-in-memory"><code>given Character</code>在内存中的布局</h3>
<p>现在想象我们有一个这样的<code>Character</code>：</p>
<pre><code>let ch = Character("Duchamp", "Dadaist", 22)</code></pre>
<p>角色<code>ch</code>在内存中的布局大概像这样（只关注<code>name</code>字段）：</p>
<pre><code>[栈帧]                    [堆]
ch: Character {
    _flag: 1
    name: String {
        _flag: 1         { _ref_count: 1
        buffer: ──────────►'D'
        initialized: 7     ...
        capacity: 8        'p' }
    }
    klass: ...
    hp: 22
}
</code></pre><p>让我们解释一下。首先，每个对象在内存中都是扁平布局的，就像你在Rust中看到的那样。因此<code>ch</code>的字段存储在栈上，而<code>name</code>字段就在其中扁平布局。</p>
<p>每个拥有其他对象的对象都以一个隐藏字段<code>_flag</code>开始。此字段指示对象是否共享（未来我们将添加更多值以解释其他权限）。如果字段为1，则对象未共享。如果为2，则共享。</p>
<p>堆分配的对象（即使用<code>Pointer[]</code>）以引用计数开始，然后才是实际数据（实际上是在偏移量-4处）。在这种情况下，我们有一个<code>Pointer[char]</code>，因此随后的实际数据只是简单的字符。</p>
<h3 id="layout-of-a-shared-character-in-memory"><code>shared Character</code>在内存中的布局</h3>
<p>如果我改为创建一个<em>共享</em>角色：</p>
<pre><code>let ch1 = Character("Duchamp", "Dadaist", 22).share
          #                                   -----</code></pre>
<p>内存布局将相同，但角色上的标志字段现在是2：</p>
<pre><code>[栈帧]                    [堆]
ch: Character {
    _flag: 2 👈 (现在是 2 了!)
    name: String {
        _flag: 1         { _ref_count: 1
        buffer: ──────────►'D'
        initialized: 7     ...
        capacity: 8        'p' }
    }
    klass: ...
    hp: 22
}
</code></pre><h3 id="copying-a-shared-character">复制一个<code>shared Character</code></h3>
<p>现在想象我们创建了同一个共享角色的两个副本：</p>
<pre><code>let ch1 = Character("Duchamp", "Dadaist", 22).share
let ch2 = ch1</code></pre>
<p>发生的情况是，我们将复制<code>_ch1</code>的所有字段，然后因为<code>_flag</code>是2，我们将递增其中堆分配数据的引用计数：</p>
<pre><code>[栈帧]                    [堆]
ch1: Character {
    _flag: 2
    name: String {
        _flag: 1         { _ref_count: 2
        buffer: ────────┬─►'D'        👆
        initialized: 7  │  ...      (现在是
        capacity: 8     │  'p' }     2 了!)
    }                   │
    class: ...          │
    hp: 22              │
}                       │
                        │
ch2: Character {        │
    _flag: 2            │
    name: String {      │
        _flag: 1        │
        buffer: ────────┘
        initialized: 7
        capacity: 8
    }
    class: ...
    hp: 22
}
</code></pre><h3 id="copying-out-the-name-field">复制出name字段</h3>
<p>现在想象我们要复制出<em>name</em>字段，而不是整个角色：</p>
<pre><code>let ch1 = Character("Duchamp", "Dadaist", 22).share
let name = ch1.name</code></pre>
<p>……发生的是：</p>
<ol>
<li>遍历<code>ch1</code>，我们观察到<code>_flag</code>字段是2，因此<code>ch1</code>是共享的</li>
<li>我们从<code>name</code>中复制出<code>String</code>字段。因为角色是共享的：
<ul>
<li>我们将新字符串上的<code>_flag</code>字段修改为2</li>
<li>我们为任何堆值递增引用计数</li>
</ul>
</li>
</ol>
<p>结果是你得到：</p>
<pre><code>[栈帧]                    [堆]
ch1: Character {
    _flag: 2
    name: String {
        _flag: 1         { _ref_count: 2
        buffer: ────────┬─►'D'
        initialized: 7  │  ...
        capacity: 8     │  'p' }
    }                   │
    class: ...          │
    hp: 22              │
}                       │
                        │
name: String {          │
    _flag: 2            │
    buffer: ────────────┘
    initialized: 7
    capacity: 8
}
</code></pre><h2 id="sharing-propagation-is-one-example-of-permission-propagation">“共享传播”是权限传播的一个例子</h2>
<p>这篇文章展示了达达中的<code>shared</code>值如何工作，并展示了当你访问字段时<code>shared</code>权限如何<em>传播</em>。<em>权限</em>是达达管理对象生命周期的方式。到目前为止我们已经看到了两种：</p>
<ul>
<li><code>given</code>权限表示唯一拥有的值（用Rust的术语来说是<code>T</code>）；</li>
<li><code>shared</code>权限表示可复制的值（最接近的Rust等价物是<code>Arc&lt;T&gt;</code>）。</li>
</ul>
<p>在未来的文章中，我们将看到<code>ref</code>和<code>mut</code>权限，它们大致对应于<code>&amp;</code>和<code>&amp;mut</code>，并讨论整个系统如何协同工作。</p>
<h2 id="dada-is-more-than-a-pretty-face">达达不仅仅是一个漂亮的外表</h2>
<p>这是我们开始看到达达更多特点的第一篇文章。回顾之前的几篇文章，你可能会认为达达只是在熟悉的Rust语法之上套了一个可爱的语法。但正如你从<code>shared</code>的工作方式所看到的，达达远不止于此。</p>
<p>我倾向于将达达视为某种意义上的“固执己见的Rust”。与Rust不同，它对做事方式强加了一些标准。例如，每个对象（至少每个具有堆分配字段的对象）都有一个<code>_flag</code>字段。每个堆分配都有一个引用计数。</p>
<p>这些约定带来了一些适度的运行时成本。我的规则是，基本操作允许执行“浅层”操作，例如切换<code>_flag</code>或调整每个字段的引用计数。但它们不能执行需要遍历堆结构的“深层”操作。</p>
<p>作为采用这些约定并支付这些成本的交换，你得到了“可组合性”，我的意思是达达中的权限（如<code>shared</code>）流动得更加自然，并且语义上等价的类型（即你可以用它们做相同事情的类型）通常在内存中具有相同的布局。</p>
<div>
<hr>
<ol>
<li id="fn:1">
<p>记住我还没有实现所有这些，我是从笔记本中的记忆和笔记中汲取灵感。我保留在我实施过程中更改任何和所有内容的权利。&nbsp;<a href="#fnref:1" rel="noopener noreferrer">↩︎</a></p>
</li>
</ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 14972 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2026/02/14/sharing-in-dada/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2026/02/14/sharing-in-dada/</guid>
      <pubDate>Sat, 14 Feb 2026 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Ubuntu 采用 Rust 意味着什么</title>
      <description>[AI 摘要] Ubuntu 采用 Rust 有助于该语言跨越技术鸿沟，成为主流选择，但这需要 Rust 社区适应并倾听新采用者的需求。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> Ubuntu 采用 Rust 有助于该语言跨越技术鸿沟，成为主流选择，但这需要 Rust 社区适应并倾听新采用者的需求。</div><p>好的，我刚从 Rust Nation 回来，正忙着用（公认很糟糕的）英语口音尝试吓唬我十几岁的女儿<sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup>。这是一次很棒的旅行，有很多好的对话和一些有趣的观察。我打算就其中一些内容写博客，从 Jon Seager 的闭幕主题演讲 "Rust Adoption At Scale with Ubuntu" 激发的一些思考开始。</p>
<h2 id="there-are-many-chasms-out-there">世界上有许多鸿沟</h2>
<p>一段时间以来，我一直在自我辩论，Rust 是否已经<a href="https://en.wikipedia.org/wiki/Crossing_the_Chasm" rel="noopener noreferrer">"跨越了鸿沟"</a>？如果你不熟悉这个术语，它来自一本书，该书以一种"流行科学"的方式介绍了<a href="https://en.wikipedia.org/wiki/Technology_adoption_life_cycle" rel="noopener noreferrer">技术采用生命周期</a>。</p>
<p>答案当然是<em>取决于你问谁</em>。在我最了解的 Amazon 内部，答案是我们"大部分已经跨越了"：Rust 已稳固确立为构建大规模数据平面或资源感知代理的正确方式，并且越来越被视为设备和机器人领域底层代码的正确选择——但仍然存在一种挥之不去的看法，认为 Rust 对"那些在 S3（或其他地方）的奇才开发者"很有用，但对于更普通的开发则有点杀鸡用牛刀<sup id="fnref:3"><a href="#fn:3" rel="noopener noreferrer">3</a></sup>。</p>
<p>另一方面，在安全关键软件领域，正如 Pete LeVasseur 在最近的 rust-lang 博客文章中所写，Rust 仍在努力争取立足之地。有一些成功的产品，但大多数行业仍处于"观望"模式，让早期采用者铺平道路。</p>
<h2 id="crossing-the-chasm-means-finding-reference-customers">“跨越鸿沟”意味着找到“标杆客户”</h2>
<p>至少我从阅读<a href="https://en.wikipedia.org/wiki/Crossing_the_Chasm" rel="noopener noreferrer">《跨越鸿沟》</a>和关于<a href="https://en.wikipedia.org/wiki/Technology_adoption_life_cycle" rel="noopener noreferrer">技术采用生命周期</a>的其他资料中得到的主要观点是需要"标杆客户"。当你刚开始接触新事物时，你寻找的是被新事物吸引的先驱者和早期采用者：</p>
<blockquote>
<p>早期采用者购买的是某种<em>变革推动剂</em>。通过成为行业内率先实施这一变革的人，早期采用者期望在竞争中抢先一步。——摘自《跨越鸿沟》</p>
</blockquote>
<p>但随着你的技术成熟，你必须说服风险承受能力越来越低的人：</p>
<blockquote>
<p>早期主流人群希望购买现有运营的<em>效率提升</em>。他们希望最小化与旧有方式的断裂。他们想要的是进化，而非革命。——摘自《跨越鸿沟》</p>
</blockquote>
<p>那么，对人们来说最令人信服去尝试新事物的是什么？答案是看到其他与他们类似的人已经成功了。</p>
<p>你可以在 Amazon 的例子和安全关键软件的例子中看到这一点。显然，在网络服务中看到 Rust 的使用并不意味着它已准备好用于你汽车的转向柱<sup id="fnref:4"><a href="#fn:4" rel="noopener noreferrer">4</a></sup>。即使在网络服务领域，看到像 S3 这样的团队用 Rust 取得成功，可能会说服其他构建大规模服务的团队尝试 Rust，但不一定能说服一个团队在他们的下一个 CRUD 服务中使用 Rust。坦率地说，这也不应该说服他们！他们很可能会遇到障碍。</p>
<h2 id="ubuntu-is-helping-rust-cross-the-user-land-linux-chasm">Ubuntu 正在帮助 Rust 跨越“（用户态 Linux）鸿沟”</h2>
<p>所有这些想法在我观看 Canonical 公司（Ubuntu 背后的公司）工程副总裁 Jon Seager 的主题演讲时都在我脑海中浮现。类似于 Lars Bergstrom 多年前关于 Google 内部 Rust 采用的<a href="https://www.youtube.com/watch?v=QrrH2lcl9ew" rel="noopener noreferrer">史诗级演讲</a>，Jon 阐述了 Canonical 采用 Rust 的理由，这个理由既<strong>富有远见</strong>又<strong>极其务实</strong>。</p>
<p>"富有远见而又极其务实"几乎是我们从<em>早期采用者</em>跨越到<em>早期主流</em>所需事物的教科书式描述。我们需要的是那些首先关心交付正确结果，但对可能帮助他们做得更好的新想法持开放态度的人；那些能够同时站在鸿沟两侧的人。</p>
<p>Jon 描述了 Canonical 如何将其自身的开发集中在少数几种语言上：Python、C/C++ 和 Go，以及他们最近如何引入 Rust 并将其作为新的<a href="https://smallcultfollowing.com/babysteps/blog/2025/03/10/rust-2025-intro/" rel="noopener noreferrer">基础设施工作</a>的首选语言，以取代 C、C++ 和（部分使用的）Python。</p>
<h2 id="ubuntu-is-building-the-bridge-across-the-chasm">Ubuntu 正在建造跨越鸿沟的桥梁</h2>
<p>Jon 谈到他认为 Ubuntu 的工作之一是通过支持构建内存安全的基础设施工具来"向前投资"。Jon 所说的支持既包括财务上的——Canonical 赞助了 <a href="https://trifectatech.org/" rel="noopener noreferrer">Trifecta Tech Foundation</a> 开发 <a href="https://github.com/trifectatechfoundation/sudo-rs" rel="noopener noreferrer">sudo-rs</a> 和 <a href="https://github.com/pendulum-project/ntpd-rs" rel="noopener noreferrer">ntpd-rs</a>，并赞助了 <a href="https://github.com/uutils/" rel="noopener noreferrer">uutils org</a> 的 <a href="https://uutils.github.io/coreutils/" rel="noopener noreferrer">coreutils</a> 工作——也包括声誉上的。Ubuntu 可以承担尝试新事物的风险，证明其可行，然后让其他人受益。</p>
<p>还记得《跨越鸿沟》这本书如何描述早期主流人群吗？他们"希望最小化与旧有方式的断裂"。还有什么比拥有可直接替换的、能融入他们现有工作流的工具更好的方式呢？</p>
<h2 id="the-challenge-for-rust-listening-to-these-new-adopters">Rust 的挑战：倾听这些新采用者</h2>
<p>新的采用带来了新的视角。周四晚上，我参加了一个由 Ernest Kissiedu<sup id="fnref:6"><a href="#fn:6" rel="noopener noreferrer">6</a></sup> 组织的晚宴<sup id="fnref:5"><a href="#fn:5" rel="noopener noreferrer">5</a></sup>。Jon Seager 和来自各行各业的其他一些 Rust 采用者在场，还有来自 Rust 基金会和开源项目的其他几位成员。</p>
<p>Ernest 请他们给我们谈谈对 Rust 的真实看法。Jon 提出了一个挑衅性的评论，即我们需要重新审视关于拥有小型标准库的政策。他不是第一个这样说的人，这是我们多年来一直听到的事情——而且我认为他是对的！虽然我认为答案不仅仅是发布一个庞大的标准库。事实上，这恰好为（我希望能够）写的下一篇关于一个我称之为"电池组"<sup id="fnref:7"><a href="#fn:7" rel="noopener noreferrer">7</a></sup> 的项目的博客文章做了铺垫。</p>
<h2 id="to-grow-you-have-to-change">要成长，就必须改变</h2>
<p>更广泛的观点是，从面向"先驱者"和"早期采用者"转变为面向"早期主流"，有时需要一些令人不适的改变：</p>
<blockquote>
<p>任何两个采用群体之间的过渡通常都极其尴尬，因为你必须在旧有策略最得心应手的时候采用新的策略。[...] 如果这家高科技公司在刚刚取得面向远见者的营销成功后，忽略了改变其销售说辞，情况可能会进一步复杂化。[...] <strong>公司可能说的是"最先进的"，而实用主义者想听的是"行业标准"。</strong> ——摘自《跨越鸿沟》（强调为本人所加）</p>
</blockquote>
<p>不是所有人都会记得，但在 2016 年有一个名为<a href="https://internals.rust-lang.org/t/proposal-the-rust-platform/3745" rel="noopener noreferrer">“Rust 平台”</a>的提案。其想法是引入一些 crate 并将其作为某种"扩展标准库"来认可。人们<em>讨厌</em>它。毕竟，他们说，为什么不直接在你的 <code>Cargo.toml</code> 中添加依赖呢？这很容易。老实说，他们是对的——至少在当时是这样。</p>
<p>我认为 "Rust 平台"是一个很好的例子，说明了某些东西如何不适合早期采用者（他们想要最新最好的东西，不介意自己寻找最佳的 crate），但可能非常适合早期主流人群<sup id="fnref:8"><a href="#fn:8" rel="noopener noreferrer">8</a></sup>。</p>
<p>无论如何，我写这篇文章不是为了支持或反对某件事，而是为了表达一种理念：我们必须对将我们学到的智慧适应新环境持开放态度。过去，我们试图让 Rust 进入行业的意识——我们已经成功了。</p>
<p>我们现在面临的任务不同：<strong>我们需要让 Rust 成为最佳选择，不仅是在"它<em>可能成为</em>"的方面，更是在"它<em>实际上</em>"的方面</strong>——有时这两者是紧张的。</p>
<h2 id="another-challenge-for-rust-turning-adoption-into-investment">Rust 的另一个挑战：将采用转化为投资</h2>
<p>晚宴后半段，话题像往常一样转向了资金。Rust 采用的增长也意味着 Rust 项目及其生态系统面临的需求增长。我们如何将这些点连接起来？这一直是我思考的一个重点，在写这段话时我意识到关于这个话题我还有多少博客文章要写，但让我先概述一下这次晚宴和最近其他场合提出的一些有趣观点。</p>
<h2 id="investment-can-mean-contribution-particularly-for-open-source-orgs">投资可以意味着贡献，尤其是对于开源组织</h2>
<p>首先，提供支持的方式不止金钱。对于 Canonical 这样彻头彻尾的开源组织，我最希望的是建立我们组织之间更牢固的关系。对于 Rust for Linux 开发者来说，早期 Rust 维护者代表 RfL 开发者优先处理和修复 bug，但现在越来越多的是 RfL 开发者自己修复问题，Rust 维护者担任导师角色。这太棒了！</p>
<h2 id="money-often-comes-before-a-company-has-adopted-rust-not-after">资金往往在公司采用 Rust 之前到来，而非之后</h2>
<p>其次，关于资金我看到了一个有趣的现象。我们通常认为公司投资于他们依赖的开源依赖项。但还有一个完全不同的资金来源，而且可能更容易获取，那就是寻找那些正在<strong>考虑</strong> Rust 但尚未采用它的公司。</p>
<p>对于那些"潜在"采用者，组织中通常有<em>个人</em>在努力为采用 Rust 做论证——这些个人是早期采用者，是对事物可能如何发展有远见的人，但他们试图向他们务实、早期主流的公司推销。为了做到这一点，他们通常有一份需要支持的"基本功能"清单；更重要的是，他们通常有一些预算来让这些事情发生。</p>
<p>这是我在与基金会的银牌成员目录负责人 Alexandru Radovici 交谈时提到的，他说许多安全关键公司有他们想花掉的钱来弥补 Rust 中的各种差距，但他们不知道如何花。Jon 对 Trifecta Tech 和 uutils org 的投资具有同样的性质：他试图弥补阻碍 Ubuntu 更广泛使用 Rust 的差距。</p>
<h2 id="conclusions">结论…？</h2>
<p>首先，你应该看看 Jon 的演讲。"精彩"，就像英国人说的那样。</p>
<p>但我的另一个主要想法是，现在是 Rust 的一个关键时刻。我们显然在多个领域从远见者和早期采用者转向务实的早期主流，我们需要意识到这样做可能需要我们改变一些一直以来的行事方式。我喜欢<a href="https://en.wikipedia.org/wiki/Crossing_the_Chasm" rel="noopener noreferrer">《跨越鸿沟》</a>中的这段话：</p>
<blockquote>
<p>要成功地向实用主义者营销，你不必成为一个实用主义者——只需理解他们的价值观并努力服务于他们。要更深入地了解这些价值观，如果说远见者的目标是实现量子跃迁，那么实用主义者的目标就是实现百分比改进——渐进的、可衡量的、可预测的进步。[...] 要向实用主义者营销，你必须要有耐心。你需要精通主导他们特定业务的问题。你需要出现在他们参加的行业特定会议和贸易展览会上。</p>
</blockquote>
<p>重读<a href="https://en.wikipedia.org/wiki/Crossing_the_Chasm" rel="noopener noreferrer">《跨越鸿沟》</a>作为撰写这篇博客文章的一部分，真正帮助我理清了 Rust 目前所处的位置——我认为在大多数情况下，我们仍在跨越鸿沟，但我们已经走了很长一段路。我认为我们看到的是一个持续的趋势，即我们有符合早期采用者"远见者"形象的 Rust<em>倡导者</em>，他们成功地在符合实用主义者、早期主流形象的公司内倡导 Rust。</p>
<h3 id="open-source-can-be-a-great-enabler-to-cross-the-chasm">开源可以成为跨越鸿沟的强大助力……</h3>
<p>对我来说，开源正是进行这种营销的一个绝佳平台。与公司不同，我们不必事事亲力亲为。我们必须利用这样一个事实：<em>开源帮助那些自助者</em>——找到那些行业中可能真正受益于 Rust 的远见者，将他们纳入 Rust 的圈子，然后（最重要的是！）<strong>支持和赋能</strong>他们以适应自己的需求。</p>
<h3 id="but-only-if-we-dont-get-too-middle-school-about-it">……但前提是我们不要对此太“中学化”</h3>
<p>最后这部分听起来可能很简单，但做起来比说的难。当你沉浸于开源时，它看起来像是一个友好、欢迎所有人的地方。但现实是，它可能充满小圈子和"口头传统"，即"每个人都知道"的事情<sup id="fnref:9"><a href="#fn:9" rel="noopener noreferrer">9</a></sup>。带着想法来的人可能会因为用了错误的词语而被否定。他们很容易将随机贡献者（或者可能只是个喷子……）的，呃，"充满激情"的评论误认为是项目领导层的官方意见。只需要一次粗鲁的回应就能把人吓跑。</p>
<h3 id="what-rust-needs-most-is-empathy">Rust 最需要的是同理心</h3>
<p>那么，最终能帮助 Rust 最大程度成功的将是什么？<a href="https://smallcultfollowing.com/babysteps/blog/2023/09/27/empathy-in-open-source/" rel="noopener noreferrer">开源中的同理心</a>。让我们走出去，找出 Rust 能在哪里帮助人们，并付诸行动。激动人心的时刻！</p>
<div>
<hr>
<ol>
<li id="fn:1">
<p>我是出了名的不擅长模仿口音。我最好的英式口音听起来更像辛普森一家里的 Apu。我真希望我能模仿出令人信服的希腊口音，但很遗憾不行。&nbsp;<a href="#fnref:1" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:2">
<p>我的另一个名言是"没有什么比临时代码更永久了"。我大学毕业后在初创公司工作时就常这么说，但多年的经验只证明它越来越正确。&nbsp;<a href="#fnref:2" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:3">
<p>Russel Cohen 和 Jess Izen 在去年的 RustConf 上就我们的团队如何帮助团队判断 Rust 对他们是否可行做了一次<a href="https://www.youtube.com/watch?v=VthhIdqwdHc" rel="noopener noreferrer">精彩演讲</a>。但自那时起，另一个产生巨大影响的是 AI，它使得重写旧系统等先前认为不可能的项目变得触手可及。&nbsp;<a href="#fnref:3" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:4">
<p>说明一下，我不知道汽车转向柱里是否有代码。我想现在应该有吧？用于助力转向之类的？&nbsp;<a href="#fnref:4" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:5">
<p>或者我应该称之为"茶点"？或者"晚饭"？我搞不懂英国的用餐时间。&nbsp;<a href="#fnref:5" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:6">
<p>Ernest 真是个让人愉快的人。他很安静，但如果你能说服他分享，他会有很多见解。如果你有机会见到他，一定要去！如果你住在伦敦，去参加伦敦 Rust 聚会！找到 Ernest 并自我介绍。告诉他 Niko 让你来的，你应该说他有多棒，你想学习他多年积累的智慧。然后看着他脸红。真是个可爱的家伙。&nbsp;<a href="#fnref:6" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:7">
<p>如果你等不及，可以在这里阅读一些<a href="https://rust-lang.zulipchat.com/#narrow/channel/220302-wg-cli/topic/Hello.20everyone/near/570148087" rel="noopener noreferrer">Zulip 讨论</a>。&nbsp;<a href="#fnref:7" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:8">
<p>我想谈论的<a href="https://rust-lang.zulipchat.com/#narrow/channel/220302-wg-cli/topic/Hello.20everyone/near/570148087" rel="noopener noreferrer">“电池组”提案</a>在某些方面与 Rust 平台相似，但更去中心化，而且在我看来通常更好——但我扯远了！&nbsp;<a href="#fnref:8" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:9">
<p><a href="https://en.wikipedia.org/wiki/Betteridge%27s_law_of_headlines" rel="noopener noreferrer">贝特里奇的标题法则</a>认为"任何以问号结尾的标题都可以用<em>不</em>来回答"。嗯，Niko 的开源法则<sup id="fnref:2"><a href="#fn:2" rel="noopener noreferrer">2</a></sup> 是"其实没有人真正知道任何'每个人'都知道的事情"。&nbsp;<a href="#fnref:9" rel="noopener noreferrer">↩︎</a></p>
</li>
</ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 10186 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2026/02/23/ubuntu-rustnation/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2026/02/23/ubuntu-rustnation/</guid>
      <pubDate>Mon, 23 Feb 2026 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Dada如何实现内部引用</title>
      <category>dada</category>
      <description>[AI 摘要] 文章阐述了Dada编程语言如何通过基于位置的权限系统实现内部引用，从而解决了Rust中借用数据无法移动的限制。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 文章阐述了Dada编程语言如何通过基于位置的权限系统实现内部引用，从而解决了Rust中借用数据无法移动的限制。</div><p>在我之前的Dada博文中，我讨论了Dada如何实现可组合共享。今天，我将开始深入探讨Dada的<em>权限</em>系统；权限是Dada中相当于Rust借用检查器的机制。</p>
<h2 id="goal-richer-place-based-permissions">目标：更丰富的基于位置的权限</h2>
<p>Dada旨在通过使用基于位置的权限来超越Rust的能力。Dada允许你编写函数和类型，它们能同时捕获一个<em>值</em>和<em>从该值借用的东西</em>。</p>
<p>举个有趣的例子，假设你正在编写一些Rust代码来处理逗号分隔的列表，只查找长度为5或更长的条目：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>list</span>: <span>String</span> <span>=</span><span> </span><span>format!</span><span>(</span><span>"...something big, with commas..."</span><span>);</span><span>
</span></span></span><span><span><span>let</span><span> </span><span>items</span>: <span>Vec</span><span>&lt;&amp;</span><span>str</span><span>&gt;</span><span> </span><span>=</span><span> </span><span>list</span><span>
</span></span></span><span><span><span>    </span><span>.</span><span>split</span><span>(</span><span>","</span><span>)</span><span>
</span></span></span><span><span><span>    </span><span>.</span><span>map</span><span>(</span><span>|</span><span>s</span><span>|</span><span> </span><span>s</span><span>.</span><span>trim</span><span>())</span><span> </span><span>// 去除空白字符
</span></span></span><span><span><span>    </span><span>.</span><span>filter</span><span>(</span><span>|</span><span>s</span><span>|</span><span> </span><span>s</span><span>.</span><span>len</span><span>()</span><span> </span><span>&gt;</span><span> </span><span>5</span><span>)</span><span>
</span></span></span><span><span><span>    </span><span>.</span><span>collect</span><span>();</span><span>
</span></span></span></code></pre></div><p>Rust的酷炫之处在于这段代码看起来很像Python或JavaScript等高级语言，但在那些语言中，<code>split</code>调用将进行大量工作，因为它必须分配大量小字符串并复制数据。但在Rust中，<code>&amp;str</code>值只是指向原始字符串的指针，因此<code>split</code>非常高效。我喜欢这一点。</p>
<p>另一方面，假设你想将其中一些值连同原始字符串打包，并发送到另一个线程进行处理。你可能认为你可以创建一个像这样的结构体……</p>
<div><pre><code><span><span><span>struct</span> <span>Message</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>list</span>: <span>String</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>items</span>: <span>Vec</span><span>&lt;&amp;</span><span>str</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>//         ----
</span></span></span><span><span><span>    </span><span>// 目标是持有一个引用
</span></span></span><span><span><span>    </span><span>// 指向list中的字符串
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>……然后创建列表和项目并将它们存入其中：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>list</span>: <span>String</span> <span>=</span><span> </span><span>format!</span><span>(</span><span>"...something big, with commas..."</span><span>);</span><span>
</span></span></span><span><span><span>let</span><span> </span><span>items</span>: <span>Vec</span><span>&lt;&amp;</span><span>str</span><span>&gt;</span><span> </span><span>=</span><span> </span><span>/* 如前所述 */</span><span>;</span><span>
</span></span></span><span><span><span>let</span><span> </span><span>message</span><span> </span><span>=</span><span> </span><span>Message</span><span> </span><span>{</span><span> </span><span>list</span><span>,</span><span> </span><span>items</span><span> </span><span>};</span><span>
</span></span></span><span><span><span>//                      ----
</span></span></span><span><span><span>//                        |
</span></span></span><span><span><span>// 这会*移动*`list`到结构体中。
</span></span></span><span><span><span>// 这又会使`items`失效，而`items`
</span></span></span><span><span><span>// 是从`list`借用的，因此无法
</span></span></span><span><span><span>// 构造`Message`。
</span></span></span></code></pre></div><p>但正如经验丰富的Rust开发者所知，这行不通。当你有像<code>&amp;str</code>这样的借用数据时，这些数据不能被移动。如果你想处理这种情况，你需要从<code>&amp;str</code>转换为发送索引、拥有的字符串或其他解决方案。唉！</p>
<h2 id="dadas-permissions-use-places-not-lifetimes">Dada的权限使用<em>位置</em>，而非<em>生命周期</em></h2>
<p>Dada的处理方式有些不同。首先，当你创建引用时，产生的类型命名的是<em>数据被借用的位置</em>，而非<em>引用的生命周期</em>。因此，<code>items</code>的类型注解会是<code>ref[list] String</code><sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup>（至少，如果你想写出完整细节而不是留给类型推断器的话）：</p>
<pre><code>let list: given String = "...something big, with commas..."
let items: given Vec[ref[list] String] = list
    .split(",")
    .map(_.trim()) // 去除空白字符
    .filter(_.len() &gt; 5)
    //      ------- 我*认为*这是闭包的语法？
    //              我忘了当初的想法，还没实现。
    .collect()</code></pre>
<p>我之前在博客中谈到过<a href="https://smallcultfollowing.com/babysteps/blog/2024/03/04/borrow-checking-without-lifetimes/" rel="noopener noreferrer">我如何想在Rust中重新定义生命周期为位置</a>，因为我觉得像<code>ref[list] String</code>这样的类型更容易教学和解释：你不必解释生命周期引用代码的某个部分，而是可以说“这是一个引用变量<code>list</code>的<code>String</code>”。</p>
<p>但更酷的是，命名位置为更灵活的借用打开了大门。在Dada中，如果你想打包列表和项目，你可以像这样构建<code>Message</code>类型：</p>
<pre><code>class Message(
    list: String
    items: Vec[ref[self.list] String]
    //             ---------
    //   从另一个字段借用！
)

// 如前所述：
let list: String = "...something big, with commas..."
let items: Vec[ref[list] String] = list
    .split(",")
    .map(_.strip()) // 去除空白字符
    .filter(_.len() &gt; 5)
    .collect()

// 创建消息，有趣的部分在这里！
let message = Message(list.give, items.give)</code></pre>
<p>注意最后一行——<code>Message(list.give, items.give)</code>。我们可以创建一个新的类，并将<code>list</code>移动到其中，<em>同时</em>移动<code>items</code>，而<code>items</code>是从list借用的。很巧妙，对吧？</p>
<p>好的，让我们回顾一下这一切是如何工作的。</p>
<h2 id="references-in-dada-are-the-default">Dada中的引用是默认行为</h2>
<p>让我们从语法开始。在深入<code>Message</code>示例之前，我想回到之前文章中的<code>Character</code>示例，因为它解释起来更简单。这是一些Rust代码，声明了一个<code>Character</code>结构体，创建了它的一个拥有副本，然后获取了它的几个引用。</p>
<div><pre><code><span><span><span>struct</span> <span>Character</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>name</span>: <span>String</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>class</span>: <span>String</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>hp</span>: <span>u32</span><span>,</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>let</span><span> </span><span>ch</span>: <span>Character</span><span> </span><span>=</span><span> </span><span>Character</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>name</span>: <span>format</span><span>!</span><span>(</span><span>"Ferris"</span><span>),</span><span>
</span></span></span><span><span><span>    </span><span>class</span>: <span>format</span><span>!</span><span>(</span><span>"Rustacean"</span><span>),</span><span>
</span></span></span><span><span><span>    </span><span>hp</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>let</span><span> </span><span>p</span>: <span>&amp;</span><span>Character</span><span> </span><span>=</span><span> </span><span>&amp;</span><span>ch</span><span>;</span><span>
</span></span></span><span><span><span>let</span><span> </span><span>q</span>: <span>&amp;</span><span>String</span> <span>=</span><span> </span><span>&amp;</span><span>p</span><span>.</span><span>name</span><span>;</span><span>
</span></span></span></code></pre></div><p>Dada中等效的代码如下所示：</p>
<pre><code>class Character(
    name: String,
    klass: String,
    hp: u32,
)

let ch: Character = Character("Tzara", "Dadaist", 22)
let p: ref[ch] Character = ch
let q: ref[p] String = p.name</code></pre>
<p>首先要注意的是，在Dada中，当你命名一个变量或位置时，<strong>默认</strong>行为是创建一个引用。因此<code>let p = ch</code>不会像Rust那样移动<code>ch</code>，而是创建一个对存储在<code>ch</code>中的<code>Character</code>的引用。你也可以显式地写<code>let p = ch.ref</code>，但这不是首选。类似地，<code>let q = p.name</code>创建了一个对字段<code>name</code>中值的引用。（如果你想<em>移动</em>这个角色，你会写<code>let ch2 = ch.give</code>，而不是像Rust中那样写<code>let ch2 = ch</code>。）</p>
<p>注意我说<code>let p = ch</code>“创建了一个对存储在<code>ch</code>中的<code>Character</code>的引用”。特别地，我<em>没有</em>说“创建了一个对<code>ch</code>的引用”。这是一个微妙的措辞选择，但有着重大的影响。</p>
<h2 id="references-in-dada-are-not-pointers">Dada中的引用不是指针</h2>
<p>我之所以写<code>let p = ch</code>“创建了一个对存储在<code>ch</code>中的<code>Character</code>的引用”而不是“创建了一个对<code>ch</code>的引用”，是因为在Dada中，<em>引用不是指针</em>。相反，它们是值的浅拷贝，非常像我们在上一篇文章中看到的<code>shared Character</code>虽然<em>行为</em>像<code>Arc&lt;Character&gt;</code>，但表示为浅拷贝。</p>
<p>所以在Rust中，下面这段代码……</p>
<div><pre><code><span><span><span>let</span><span> </span><span>ch</span><span> </span><span>=</span><span> </span><span>Character</span><span> </span><span>{</span><span> </span><span>..</span><span>.</span><span> </span><span>};</span><span>
</span></span></span><span><span><span>let</span><span> </span><span>p</span><span> </span><span>=</span><span> </span><span>&amp;</span><span>ch</span><span>;</span><span>
</span></span></span><span><span><span>let</span><span> </span><span>q</span><span> </span><span>=</span><span> </span><span>&amp;</span><span>ch</span><span>.</span><span>name</span><span>;</span><span>
</span></span></span></code></pre></div><p>……在内存中看起来像这样……</p>
<pre><code>        # Rust内存表示

            栈空间                     堆空间
            ─────                       ────

┌───► ch: Character {
│ ┌───► name: String {
│ │         buffer: ───────────► "Ferris"
│ │         长度: 6
│ │         容量: 12
│ │     },
│ │     ...
│ │   }
│ │   
└──── p
  │
  └── q
</code></pre><p>而在Dada中，像这样的代码</p>
<pre><code>let ch = Character(...)
let p = ch
let q = ch.name</code></pre><p>会看起来像这样</p>
<pre><code># Dada内存表示

栈空间                     堆空间
─────                     ────

ch: Character {
    name: String {
            buffer: ───────┬───► "Ferris"
            长度: 6        │
            容量: 12       │
    },                     │
    ..                     │
}                          │
                           │
p: Character {             │
    name: String {         │
            buffer: ───────┤
            长度: 6        │
            容量: 12       │
    ...                    │
}                          │
    }                      │
                           │
q: String {                │
    buffer: ───────────────┘
    长度: 6
    容量: 12
}
</code></pre><p>显然，Dada的表示在栈上占用了更多内存。但注意它<em>没有</em>复制堆中的内存，而堆通常是绝大多数数据所在的地方。</p>
<h2 id="dada-talks-about-values-not-references">Dada谈论的是<em>值</em>而非<em>引用</em></h2>
<p>这触及了一些重要的东西。像C一样，Rust将指针视为一等公民。所以给定<code>x: &amp;String</code>，<code>x</code>指的是<em>指针</em>，而<code>*x</code>指的是它的指称物<code>String</code>。</p>
<p>像Java一样，Dada走了另一条路。<code>x: ref String</code><em>就是</em>一个<code>String</code>值——包括内存表示！<code>given String</code>、<code>shared String</code>和<code>ref String</code>之间的区别不在于它们的内存布局，它们都是相同的，而在于它们是否<strong>拥有其内容</strong>。<sup id="fnref:2"><a href="#fn:2" rel="noopener noreferrer">2</a></sup></p>
<p>所以在Dada中，没有<code>*x</code>操作来从“指针”到“指称物”。这没有意义。你的变量总是包含一个字符串，但你使用该字符串的权限会改变。</p>
<p>实际上，目标是让人们在学习Dada时<em>不必</em>学习内存表示，你应该能够像在Java或Python中那样将Dada变量视为堆上的所有对象，即使它们实际上是存储在栈上的。<sup id="fnref:3"><a href="#fn:3" rel="noopener noreferrer">3</a></sup></p>
<h2 id="rust-does-not-permit-moves-of-borrowed-data">Rust不允许移动借用的数据</h2>
<p>在Rust中，你不能在值被借用时移动它们。所以如果你有像这样的代码，将<code>ch</code>移动到<code>ch1</code>中……</p>
<div><pre><code><span><span><span>let</span><span> </span><span>ch</span><span> </span><span>=</span><span> </span><span>Character</span><span> </span><span>{</span><span> </span><span>..</span><span>.</span><span> </span><span>};</span><span>
</span></span></span><span><span><span>let</span><span> </span><span>name</span><span> </span><span>=</span><span> </span><span>&amp;</span><span>ch</span><span>.</span><span>name</span><span>;</span><span> </span><span>// 创建引用
</span></span></span><span><span><span>let</span><span> </span><span>ch1</span><span> </span><span>=</span><span> </span><span>ch</span><span>;</span><span>        </span><span>// 移动`ch`
</span></span></span></code></pre></div><p>……那么这段代码只有在<code>name</code>不再被使用时才能编译：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>ch</span><span> </span><span>=</span><span> </span><span>Character</span><span> </span><span>{</span><span> </span><span>..</span><span>.</span><span> </span><span>};</span><span>
</span></span></span><span><span><span>let</span><span> </span><span>name</span><span> </span><span>=</span><span> </span><span>&amp;</span><span>ch</span><span>.</span><span>name</span><span>;</span><span> </span><span>// 创建引用
</span></span></span><span><span><span>let</span><span> </span><span>ch1</span><span> </span><span>=</span><span> </span><span>ch</span><span>;</span><span>        </span><span>// 错误：借用时不能移动
</span></span></span><span><span><span>let</span><span> </span><span>name1</span><span> </span><span>=</span><span> </span><span>name</span><span>;</span><span>    </span><span>// 再次使用引用
</span></span></span></code></pre></div>
<h2 id="but-dada-can">……但Dada可以</h2>
<p>Rust禁止移动借用的数据有两个原因：</p>
<ul>
<li>引用是指针，因此这些指针可能会失效。在上面的例子中，<code>name</code>指向<code>ch</code>的栈空间，所以如果<code>ch</code>被移动到<code>ch1</code>中，这会使引用失效。</li>
<li>类型系统会失去跟踪。在内部，Rust借用检查器有一种“间接”关系。它知道<code>ch</code>在代码的某个范围内被借用（一个“生命周期”），并且它知道<code>name</code>类型中的生命周期与该生命周期相关，但它并不真正知道<code>name</code>是从<code>ch</code>借用的。<sup id="fnref:4"><a href="#fn:4" rel="noopener noreferrer">4</a></sup></li>
</ul>
<p>这些都不适用于Dada：</p>
<ul>
<li>因为引用不是指向栈的指针，而是浅拷贝，所以移动借用的值不会使它们的内容失效。它们保持有效。</li>
<li>因为Dada的类型引用的是实际的变量名，我们可以修改它们以反映移动。</li>
</ul>
<h2 id="dada-tracks-moves-in-its-types">Dada在其类型中跟踪移动</h2>
<p>好的，让我们重温那个在Rust中会出错的示例。当我们将它转换为Dada时，我们发现它类型检查完全正常：</p>
<pre><code>class Character(...) // 如前所述
let ch: given Character = Character(...)
let name: ref[ch.name] String = ch.name
//            -- 最初是从`ch`借用的
let ch1 = ch.give
//        ------- 但`ch`被移动到了`ch1`
let name1: ref[ch1.name] = name
//             --- 现在是从`ch1`借用</code></pre>
<p>哇，很巧妙！我们可以看到，当我们从<code>ch</code>移动到<code>ch1</code>时，编译器会更新周围变量的类型。所以实际上<code>name</code>的类型变为了<code>ref[ch1.name] String</code>。然后当我们从<code>name</code>移动到<code>name1</code>时，这是完全有效的。</p>
<p>在编程语言领域，将一个变量的类型从一种更新为另一种称为“强更新”。显然，当涉及控制流时，事情会变得有点复杂，例如在这种情况下：</p>
<pre><code>let ch = Character(...)
let ch1 = Character(...)
let name = ch.name
if some_condition_is_true() {
    // 在此路径上，`name`的类型变为了
    // `ref[ch1.name] String`，因此`ch`
    // 不再被视为被借用。
    ch1 = ch.give
    ch = Character(...) // 未被借用，我们可以修改
} else {
    // 在此路径上，`name`的类型
    // 保持不变，`ch`被借用。
}
// 在这里，类型被合并，所以
// `name`的类型是`ref[ch.name, ch1.name] String`。
// 因此，`ch`在这里被视为被借用。</code></pre>
<h2 id="renaming-lets-us-call-functions-with-borrowed-values">重命名允许我们使用借用的值调用函数</h2>
<p>好的，让我们迈出下一步。让我们定义一个Dada函数，它接受一个拥有的值和另一个从该值借用的值，比如名字，然后调用它：</p>
<pre><code>fn character_and_name(
    ch1: given Character,
    name1: ref[ch1] String,
) {
    // ... 执行一些操作 ...
}</code></pre>
<p>我们可以像这样调用这个函数，正如你可能预期的那样：</p>
<pre><code>let ch = Character(...)
let name = ch.name
character_and_name(ch.give, name)</code></pre>
<p>那么……这是如何工作的呢？在内部，类型检查器通过创建一个更简单的代码片段来检查函数调用的类型，本质上是这样，然后检查<em>该片段</em>的类型。这就像脱糖，但只在类型检查时。在这个更简单的片段中，有一系列<code>let</code>语句为每个参数创建临时变量。这些临时变量总是有一个从方法签名中取得的显式类型，并用每个参数的值初始化：</p>
<pre><code>// 类型检查器将`character_and_name(ch.give, name)`“脱糖”为
// 更基本的操作：
let tmp1: given Character = ch.give
    //    ---------------   -------
    //            |         来自调用
    //    来自函数签名
let tmp2: ref[tmp1.name] String = name
    //    ---------------------   ----
    //            |         来自调用
    //    来自函数签名，
    //    但重写以使用新的
    //    临时变量</code></pre>
<p>如果这通过了类型检查，那么类型检查器就知道你提供了所需类型的值，因此这是一个有效的调用。当然还有更多步骤，但这是基本思想。</p>
<p>注意，如果你提供了从错误位置借用的数据会发生什么：</p>
<pre><code>let ch = Character(...)
let ch1 = Character(...)
character_and_name(ch, ch1.name)
//                     --- 错误的位置！</code></pre>
<p>这将无法通过类型检查，因为你得到：</p>
<pre><code>let tmp1: given Character = ch.give
let tmp2: ref[tmp1.name] String = ch1.name
    //                            --------
    //       类型是`ref[ch1.name] String`，
    //       不是`ref[tmp1.name] String`</code></pre>
<h2 id="class-constructors-are-just-special-functions">类构造函数只是特殊的函数</h2>
<p>所以现在，如果我们回到最初的示例，我们可以看到<code>Message</code>示例是如何工作的：</p>
<pre><code>class Message(
    list: String
    items: Vec[ref[self.list] String]
)</code></pre>
<p>基本上，当你构造<code>Message(list, items)</code>时，从类型系统的角度来看，这“只是另一个函数调用”，只是签名中的<code>self</code>被小心处理了。</p>
<h2 id="this-is-modeled-not-implemented">这是建模，而非实现</h2>
<p>我应该明确指出，这个系统是在<a href="https://github.com/dada-lang/dada-model/" rel="noopener noreferrer">dada-model</a>仓库中建模的，该仓库实现了一种“迷你Dada”，它包含了我认为最有趣的部分。我正在努力进一步完善这个模型，但它已经包含了我在这里展示的大部分内容。<sup id="fnref:5"><a href="#fn:5" rel="noopener noreferrer">5</a></sup>例如，<a href="https://github.com/dada-lang/dada-model/blob/b6833b57af8f0b293755410760c240b75fbf4998/src/type_system/tests/new_with_self_references.rs#L61-L99" rel="noopener noreferrer">这是一个测试</a>，当你给错误的值提供引用时，你会得到错误。</p>
<p>“真正的实现”落后很多，还没有真正处理有趣的部分。将其从模型扩展到真正的实现涉及解决类型推断和其他一些棘手的挑战，我还没有达到那一步——尽管我在编译器架构方面也有一些相当有趣的实验正在进行。<sup id="fnref:6"><a href="#fn:6" rel="noopener noreferrer">6</a></sup></p>
<h2 id="this-could-apply-to-rust">这可以应用于Rust</h2>
<p>我相信我们可以将这个系统的大部分应用于Rust。显然，我们需要重新设计借用检查器以基于位置，但这是直接的部分。更难的部分是<code>&amp;T</code>在Rust中是指针，这一点我们不容易改变。然而，对于自引用的许多用例，这并不像听起来那么重要。通常，你希望引用的数据位于堆中，因此当原始值移动时，指针实际上不会失效。</p>
<p>考虑我们开头的例子。你可能想象Rust允许这样的东西：</p>
<div><pre><code><span><span><span>struct</span> <span>Message</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>list</span>: <span>String</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>items</span>: <span>Vec</span><span>&lt;&amp;</span><span>{</span><span>self</span><span>.</span><span>list</span><span>}</span><span> </span><span>str</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>在这种情况下，<code>str</code>数据是堆分配的，因此移动字符串实际上不会使<code>&amp;str</code>值失效（有趣的是，它<em>会</em>使<code>&amp;String</code>值失效）。</p>
<p>在今天的Rust中，编译器不知道所有正在发生的事情的细节。<code>String</code>有一个<code>Deref</code>实现，因此<code>str</code>是否堆分配是相当不透明的。但我们在<a href="https://rust-lang.github.io/rust-project-goals/2026/roadmap-beyond-the-ampersand.html" rel="noopener noreferrer">超越<code>&amp;</code></a>目标中正在研究这个系统的各种变化，最值得注意的是<a href="https://rust-lang.github.io/rust-project-goals/2026/field-projections.html" rel="noopener noreferrer">字段投影</a>工作。很可能在这个背景下有机会解决这个问题，但说实话，我还没能及时跟上细节。</p>
<div>
<hr>
<ol>
<li id="fn:1">
<p>我顺便指出，Dada也将<code>str</code>和<code>String</code>统一为一个类型。我将在未来的博文详细讨论这是如何工作的。&nbsp;<a href="#fnref:1" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:2">
<p>这有点像C++引用（例如<code>String&amp;</code>），它们也“表现得好像”它们是值（即，你写<code>s.foo()</code>，而不是<code>s-&gt;foo()</code>），但C++引用确实是指针，与Dada ref不同。&nbsp;<a href="#fnref:2" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:3">
<p>这个目标部分灵感来源于我早年在亚马逊的一次对话，一位（非常有经验的）开发者告诉我，“我花了几个月才理解Rust中的变量是什么”。&nbsp;<a href="#fnref:3" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:4">
<p>几年前我在<a href="https://www.youtube.com/watch?v=_agDeiWek8w" rel="noopener noreferrer">Rust Belt Rust的Polonius演讲</a>中解释过这一点，如果你想了解更多细节。&nbsp;<a href="#fnref:4" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:5">
<p>没有闭包或迭代器链！&nbsp;<a href="#fnref:5" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:6">
<p>作为预告，我正在用异步Rust构建它，其中每个推断变量都是一个“future”，并使用“await”来找出代码的其他部分何时可能添加了约束。&nbsp;<a href="#fnref:6" rel="noopener noreferrer">↩︎</a></p>
</li>
</ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 21075 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2026/02/27/dada-internal-references/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2026/02/27/dada-internal-references/</guid>
      <pubDate>Fri, 27 Feb 2026 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>最大化极简视图类型</title>
      <category>view-types</category>
      <description>[AI 摘要] 本文提出了“视图类型”特性，旨在通过显式指定方法访问的结构体字段，解决Rust借用检查器中的借用冲突问题。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 本文提出了“视图类型”特性，旨在通过显式指定方法访问的结构体字段，解决Rust借用检查器中的借用冲突问题。</div>这篇博客文章描述了一个关于<a href="https://smallcultfollowing.com/babysteps/blog/2021/11/05/view-types/" rel="noopener noreferrer">视图类型</a>的<em>最大化极简提案</em>。它源于我在RustNation大会上与lcnr和Jack Huey的一次对话，我们讨论了各种“悬而未决”的语言改进，这些改进基本上是大家都想做的，以及要使它们成为现实需要什么。
<h2 id="example-messageprocessor">示例：MessageProcessor</h2>
<p>让我们从一个简单的例子开始。假设我们有一个结构体 <code>MessageProcessor</code>，它用一组消息创建。它将处理这些消息，并在此过程中收集一些简单的统计数据：</p>
<div><pre><code><span><span><span>pub</span><span> </span><span>struct</span> <span>MessageProcessor</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>messages</span>: <span>Vec</span><span>&lt;</span><span>String</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>statistics</span>: <span>Statistics</span><span>,</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>#[non_exhaustive]</span><span> </span><span>// 与示例无关，只是良好实践！
</span></span></span><span><span><span>pub</span><span> </span><span>struct</span> <span>Statistics</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>pub</span><span> </span><span>message_count</span>: <span>usize</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>pub</span><span> </span><span>total_bytes</span>: <span>usize</span><span>,</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>消息处理器的基本工作流程是：</p>
<ul>
<li>通过 <code>push</code> 将消息累积到 <code>self.messages</code> 向量中</li>
<li>清空累积的消息并进行处理</li>
<li>重用后备缓冲区以推送未来的消息</li>
</ul>
<h3 id="accumulating-messages">累积消息</h3>
<p>累积消息很简单：</p>
<div><pre><code><span><span><span>impl</span><span> </span><span>MessageProcessor</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>pub</span><span> </span><span>fn</span> <span>push_message</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>,</span><span> </span><span>message</span>: <span>String</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>self</span><span>.</span><span>messages</span><span>.</span><span>push</span><span>(</span><span>message</span><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></pre></div><h3 id="processing-a-single-message">处理单个消息</h3>
<p>处理单个消息的函数获取消息字符串的所有权，因为它将被发送到另一个线程。在此之前，它会更新统计数据：</p>
<div><pre><code><span><span><span>impl</span><span> </span><span>MessageProcessor</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>fn</span> <span>process_message</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>,</span><span> </span><span>message</span>: <span>String</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>self</span><span>.</span><span>statistics</span><span>.</span><span>message_count</span><span> </span><span>+=</span><span> </span><span>1</span><span>;</span><span>
</span></span></span><span><span><span>        </span><span>self</span><span>.</span><span>statistics</span><span>.</span><span>total_bytes</span><span> </span><span>+=</span><span> </span><span>message</span><span>.</span><span>len</span><span>();</span><span>
</span></span></span><span><span><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></pre></div><h3 id="draining-the-accumulated-messages">清空累积的消息</h3>
<p>你需要的最后一个函数是清空累积的消息并处理它们。编写这个函数<em>应该</em>是直接了当的，但它并非如此：</p>
<div><pre><code><span><span><span>impl</span><span> </span><span>MessageProcessor</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>pub</span><span> </span><span>fn</span> <span>process_pushed_messages</span><span>(</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>        </span><span>for</span><span> </span><span>message</span><span> </span><span>in</span><span> </span><span>self</span><span>.</span><span>messages</span><span>.</span><span>drain</span><span>(</span><span>..</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>            </span><span>self</span><span>.</span><span>process_message</span><span>(</span><span>message</span><span>);</span><span> </span><span>// &lt;-- 错误：`self` 已被借用
</span></span></span><span><span><span>        </span><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></pre></div><p>问题在于 <code>self.messages.drain(..)</code> 对 <code>self.messages</code> 进行了可变借用。当你调用 <code>self.process_message</code> 时，编译器假设你可能会修改任何字段，包括 <code>self.messages</code>。因此，它报告了一个错误。这是合理的，但令人沮丧。</p>
<p>有经验的 Rust 程序员知道一些解决方法。例如，你可以将 <code>messages</code> 字段与一个空向量交换。或者你可以调用 <code>self.messages.pop()</code>。或者你可以将 <code>process_message</code> 重写为 <code>Statistics</code> 类型上的方法。但老实说，所有这些都不够理想。上面的代码确实相当合理，如果你能以直接的方式使其工作，而不需要重构它，那就太好了。
</p><h2 id="whats-needed-a-way-for-the-borrow-checker-to-know-what-fields-a-method-may-access">需要什么：一种让借用检查器知道方法可能访问哪些字段的方法</h2>
<p>核心问题是借用检查器不知道 <code>process_message</code> 将<em>仅</em>访问 <code>statistics</code> 字段。在这篇文章中，我将专注于一种显式且相当有限的表示法，但我也将讨论我们将来如何扩展它。
</p><h2 id="view-types-extend-struct-types-with-a-list-of-fields">视图类型用字段列表扩展结构体类型</h2>
<p>视图类型的基本思想是扩展结构体类型的语法，以可选地包含一个可访问字段列表：</p>
<pre><code>RustType := StructName&lt;...&gt;
         |  StructName&lt;...&gt; { .. }         // &lt;-- 我们正在添加的
         |  StructName&lt;...&gt; { (fields),* } // &lt;-- 我们正在添加的
</code></pre><p>像 <code>MessageProcessor { statistics }</code> 这样的类型意味着“一个只能访问 <code>statistics</code> 字段的 <code>MessageProcessor</code> 结构体”。你也可以包含一个 <code>..</code>，比如 <code>MessageProcessor { .. }</code>，这意味着所有字段都可以访问，这等同于今天的结构体类型 <code>MessageProcessor</code>。
</p><h2 id="view-types-respect-privacy">视图类型尊重隐私</h2>
<p>视图类型将尊重隐私，这意味着你只能在可以命名字段 <code>messages</code> 的上下文中编写 <code>MessageProcessor { messages }</code>。
</p><h2 id="view-types-can-be-named-on-self-arguments-and-elsewhere">视图类型可以在 <code>self</code> 参数和其他地方命名</h2>
<p>你可以用这个来定义 <code>process_message</code> 只需要访问字段 <code>statistics</code>：</p>
<div><pre><code><span><span><span>impl</span><span> </span><span>MessageProcessor</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>fn</span> <span>process_message</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span> </span><span>{</span><span>statistics</span><span>},</span><span> </span><span>message</span>: <span>String</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>//             ----------------------
</span></span></span><span><span><span>        </span><span>//             `self: &amp;mut MessageProcessor {statistics}` 的简写
</span></span></span><span><span><span>        
</span></span></span><span><span><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></pre></div><p>当然，你也可以在其他参数中使用这种表示法：</p>
<div><pre><code><span><span><span>fn</span> <span>silly_example</span><span>(</span><span>..</span><span>,</span><span> </span><span>mp</span>: <span>&amp;</span><span>mut</span><span> </span><span>MessageProcessor</span><span> </span><span>{</span><span>statistics</span><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></pre></div><h2 id="explicit-view-limited-borrows">显式的视图限制借用</h2>
<p>我们还将扩展借用表达式，以便可以精确指定哪些字段可以从借用中访问：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>messages</span><span> </span><span>=</span><span> </span><span>&amp;</span><span>mut</span><span> </span><span>some_variable</span><span> </span><span>{</span><span>messages</span><span>};</span><span> </span><span>// 模糊的语法？见下文。
</span></span></span></code></pre></div><p>当你这样做时，借用检查器会生成一个类型为 <code>&amp;mut MessageProcessor {messages}</code> 的值。</p>
<p>敏锐的读者会注意到这是有歧义的。上面的代码在今天可以被解析为对一个结构体表达式的借用，比如 <code>some_variable { messages }</code>，或者更冗长地，<code>some_variable { messages: messages }</code>。我不确定该如何处理。我会在下面提到一些替代的语法，但我也注意到，编译器<em>可能</em>会以模糊的方式解析 AST，然后在名称解析结果已知时进行消歧。
</p><h2 id="we-automatically-introduce-view-borrows-in-an-auto-ref">我们在自动引用中自动引入视图借用</h2>
<p>然而，在我们的示例中，用户从不显式编写 <code>&amp;mut</code> 借用。它是由编译器在方法调用中作为自动引用添加的结果产生的：</p>
<div><pre><code><span><span><span>pub</span><span> </span><span>fn</span> <span>process_pushed_messages</span><span>(</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>    </span><span>for</span><span> </span><span>message</span><span> </span><span>in</span><span> </span><span>self</span><span>.</span><span>messages</span><span>.</span><span>drain</span><span>(</span><span>..</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>self</span><span>.</span><span>process_message</span><span>(</span><span>message</span><span>);</span><span> </span><span>// &lt;-- 自动引用发生在这里
</span></span></span><span><span><span>    </span><span>}</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>编译器会根据 <code>process_message</code> 中声明的签名，将像 <code>self.process_message(message)</code> 这样的方法调用重写为完全限定形式。今天，这会产生如下代码：</p>
<div><pre><code><span><span><span>MessageProcessor</span>::<span>process_message</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>*</span><span>self</span><span>,</span><span> </span><span>message</span><span>)</span><span>
</span></span></span></code></pre></div><p>但是因为 <code>process_message</code> 现在会声明 <code>&amp;mut self { statistics }</code>，所以我们可以改用指定字段集的借用进行脱糖：</p>
<div><pre><code><span><span><span>MessageProcessor</span>::<span>process_message</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>*</span><span>self</span><span> </span><span>{</span><span> </span><span>statistics</span><span> </span><span>},</span><span> </span><span>message</span><span>)</span><span>
</span></span></span></code></pre></div><h2 id="the-borrow-checker-would-respect-views">借用检查器会尊重视图</h2>
<p>将视图集成到借用检查器中是相当简单的。借用检查器的工作方式是，当它看到一个借用表达式时，它会在内部记录一个“贷款”，跟踪被借用的<em>位置</em>、借用的<em>方式</em>（可变、共享）以及借用的<em>生命周期</em>。我们所要做的就是为每个使用视图的借用记录多个贷款，而不是一个贷款。</p>
<p>例如，如果我们有 <code>&amp;mut self</code>，我们将记录一个 <code>self</code> 的 <code>mut</code> 贷款。但如果我们有 <code>&amp;mut self {field1, field2}</code>，我们将有两个 <code>mut</code> 贷款，一个是 <code>self.field1</code>，另一个是 <code>self.field2</code>。
</p><h2 id="example-putting-it-all-together">示例：将所有内容放在一起</h2>
<p>好的，让我们将所有内容放在一起。这是我们最初的原始示例，收集在一起：</p>
<div><pre><code><span><span><span>pub</span><span> </span><span>struct</span> <span>MessageProcessor</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>messages</span>: <span>Vec</span><span>&lt;</span><span>String</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>statistics</span>: <span>Statistics</span><span>,</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>#[non_exhaustive]</span><span>
</span></span></span><span><span><span>pub</span><span> </span><span>struct</span> <span>Statistics</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>pub</span><span> </span><span>message_count</span>: <span>usize</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>pub</span><span> </span><span>total_bytes</span>: <span>usize</span><span>,</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>impl</span><span> </span><span>MessageProcessor</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>pub</span><span> </span><span>fn</span> <span>push_message</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>,</span><span> </span><span>message</span>: <span>String</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>self</span><span>.</span><span>messages</span><span>.</span><span>push</span><span>(</span><span>message</span><span>);</span><span>
</span></span></span><span><span><span>    </span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>    </span><span>pub</span><span> </span><span>fn</span> <span>process_pushed_messages</span><span>(</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>        </span><span>for</span><span> </span><span>message</span><span> </span><span>in</span><span> </span><span>self</span><span>.</span><span>messages</span><span>.</span><span>drain</span><span>(</span><span>..</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>            </span><span>self</span><span>.</span><span>process_message</span><span>(</span><span>message</span><span>);</span><span> </span><span>// &lt;-- 错误：`self` 已被借用
</span></span></span><span><span><span>        </span><span>}</span><span>
</span></span></span><span><span><span>    </span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>    </span><span>fn</span> <span>process_message</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>,</span><span> </span><span>message</span>: <span>String</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>self</span><span>.</span><span>statistics</span><span>.</span><span>message_count</span><span> </span><span>+=</span><span> </span><span>1</span><span>;</span><span>
</span></span></span><span><span><span>        </span><span>self</span><span>.</span><span>statistics</span><span>.</span><span>total_bytes</span><span> </span><span>+=</span><span> </span><span>message</span><span>.</span><span>len</span><span>();</span><span>
</span></span></span><span><span><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></pre></div><p>今天，<code>process_pushed_messages</code> 会导致错误：</p>
<div><pre><code><span><span><span>pub</span><span> </span><span>fn</span> <span>process_pushed_messages</span><span>(</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>    </span><span>for</span><span> </span><span>message</span><span> </span><span>in</span><span> </span><span>self</span><span>.</span><span>messages</span><span>.</span><span>drain</span><span>(</span><span>..</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>//         ------------- 借用 `self.messages`
</span></span></span><span><span><span>        </span><span>self</span><span>.</span><span>process_message</span><span>(</span><span>message</span><span>);</span><span> </span><span>// &lt;-- 错误！
</span></span></span><span><span><span>        </span><span>//   --------------- 借用 `self`
</span></span></span><span><span><span>    </span><span>}</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>错误源于两个借用之间的冲突：</p>
<ul>
<li><code>self.messages.drain(..)</code> 脱糖为 <code>Iterator::drain(&amp;mut self.messages, ..)</code>，如你所见，它<code>mut</code>-借用 <code>self.messages</code>；</li>
<li>然后 <code>self.process_message(..)</code> 脱糖为 <code>MessageProcessor::process_message(&amp;mut self, ..)</code>，如你所见，它<code>mut</code>-借用了整个 <code>self</code>，这与 <code>self.messages</code> 重叠。</li>
</ul>
<p>但在“崭新的世界”里，我们将在一个地方修改程序：</p>
<div><pre><code><span><span><span>-    fn process_message(&amp;mut self, message: String) {
</span></span></span><span><span><span>+    fn process_message(&amp;mut self {statistics}, message: String) {
</span></span></span></code></pre></div><p>结果是，<code>process_pushed_messages</code> 函数现在将通过借用检查。这是因为两个贷款现在针对不同的位置：</p>
<ul>
<li>如前所述，<code>self.messages.drain(..)</code> 脱糖为 <code>Iterator::drain(&amp;mut self.messages, ..)</code>，它<code>mut</code>-借用 <code>self.messages</code>；</li>
<li>但现在，<code>self.process_message(..)</code> 脱糖为 <code>MessageProcessor::process_message(&amp;mut self {statistics}, ..)</code>，它<code>mut</code>-借用 <code>self.statistics</code>，这不与 <code>self.messages</code> 重叠。</li>
</ul>
<h2 id="at-runtime-this-is-still-just-a-pointer">在运行时，这仍然只是一个指针</h2>
<p>我想强调的一点是，“视图类型”是一种纯粹的静态构造，不会改变代码的编译方式。它们只是给借用检查器提供更多关于哪些数据将通过哪些引用访问的信息。例如，<code>process_message</code> 方法仍然只接受一个指向 <code>self</code> 的指针。</p>
<p>这与今天存在的解决方法形成对比。例如，如果我编写上面的代码，我可能会将 <code>process_message</code> 重写为一个接受 <code>&amp;mut Statistics</code> 的关联函数：</p>
<div><pre><code><span><span><span>impl</span><span> </span><span>MessageProcessor</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>fn</span> <span>process_message</span><span>(</span><span>statistics</span>: <span>&amp;</span><span>mut</span><span> </span><span>Statistics</span><span>,</span><span> </span><span>message</span>: <span>String</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>statistics</span><span>.</span><span>message_count</span><span> </span><span>+=</span><span> </span><span>1</span><span>;</span><span>
</span></span></span><span><span><span>        </span><span>statistics</span><span>.</span><span>total_bytes</span><span> </span><span>+=</span><span> </span><span>message</span><span>.</span><span>len</span><span>();</span><span>
</span></span></span><span><span><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></pre></div><p>当然，这很烦人，因为我必须编写 <code>Self::process_message(&amp;mut self.statistics, ..)</code> 而不是 <code>self.process_message()</code>，但它可以避免借用检查错误。</p>
<p>除了烦人之外，它还会改变代码的编译方式。它现在接受一个对 <code>Statistics</code> 的引用，而不是对 <code>MessageProcessor</code> 的引用。</p>
<p>在这个例子中，从一种类型改为另一种是无害的，但还有其他需要访问多个字段的例子，在这些情况下，单独传递它们效率较低。
</p><h2 id="frequently-asked-questions">常见问题解答</h2>
<h3 id="how-hard-would-this-be-to-implement">实现起来有多难？</h3>
<p>老实说，不是很困难。我认为如果我们找到一个好的贡献者愿意承担这项工作，我们今年就可以发布它。
</p><h3 id="what-about-privacy">隐私问题如何处理？</h3>
<p>我将要求视图类型中出现的字段对命名它们的代码是“可见的”（这包括通过自动引用插入的视图类型）。因此，以下内容将是一个错误：</p>
<div><pre><code><span><span><span>mod</span><span> </span><span>m</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>#[derive(Default)]</span><span>
</span></span></span><span><span><span>    </span><span>pub</span><span> </span><span>struct</span><span> </span><span>MessageProcessor</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>messages</span>: <span>Vec</span><span>&lt;</span><span>String</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>        </span><span>..</span><span>.</span><span>
</span></span></span><span><span><span>    </span><span>}</span><span>
</span></span></span><span><span><span>    
</span></span></span><span><span><span>    </span><span>impl</span><span> </span><span>MessageProcessor</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>pub</span><span> </span><span>fn</span> <span>process_message</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span> </span><span>{</span><span>messages</span><span>},</span><span> </span><span>message</span>: <span>String</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>            </span><span>//                           ----------
</span></span></span><span><span><span>            </span><span>//   在这里引用私有字段是*合法*的，但它会产生一个提示，
</span></span></span><span><span><span>            </span><span>//   就像现在公共方法接受私有类型的参数是*合法*（但会被提示）一样。
</span></span></span><span><span><span>            </span><span>//   提示的原因是，这样做实际上会使该方法无法从该模块外部调用。
</span></span></span><span><span><span>            </span><span>self</span><span>.</span><span>messages</span><span>.</span><span>push</span><span>(</span><span>message</span><span>);</span><span>
</span></span></span><span><span><span>        </span><span>}</span><span>
</span></span></span><span><span><span>    </span><span>}</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>fn</span><span> </span><span>main</span><span>()</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>mp</span><span> </span><span>=</span><span> </span><span>m</span>::<span>MessageProcessor</span>::<span>default</span><span>();</span><span>    
</span></span></span><span><span><span>    </span><span>mp</span><span>.</span><span>process_message</span><span>(</span><span>format!</span><span>(</span><span>"Hello, world!"</span><span>));</span><span>
</span></span></span><span><span><span>    </span><span>// --------------- 错误：字段 `messages` 在此处不可访问
</span></span></span><span><span><span>    </span><span>//
</span></span></span><span><span><span>    </span><span>// 这脱糖为：
</span></span></span><span><span><span>    </span><span>// 
</span></span></span><span><span><span>    </span><span>// ```
</span></span></span><span><span><span>    </span><span>// MessageProcessor::process_message(
</span></span></span><span><span><span>    </span><span>//     &amp;mut mp {messages},        // &lt;-- 命名了一个私有字段！
</span></span></span><span><span><span>    </span><span>//     format!("Hello, world!"),
</span></span></span><span><span><span>    </span><span>// )
</span></span></span><span><span><span>    </span><span>// ```
</span></span></span><span><span><span>    </span><span>// 
</span></span></span><span><span><span>    </span><span>// 这命名了私有字段 `messages`。这是一个错误。
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><h3 id="does-this-mean-that-view-types-cant-be-used-in-public-methods">这意味着视图类型不能用于公共方法吗？</h3>
<p>或多或少是这样。你可以使用它们，如果视图类型引用的是公共字段：</p>
<div><pre><code><span><span><span>#[non_exhaustive]</span><span>
</span></span></span><span><span><span>pub</span><span> </span><span>Statistics</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>pub</span><span> </span><span>message_count</span>: <span>usize</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>pub</span><span> </span><span>average_bytes</span>: <span>usize</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>// ... 也许以后会添加更多字段 ...
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>impl</span><span> </span><span>Statistics</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>pub</span><span> </span><span>fn</span> <span>total_bytes</span><span>(</span><span>&amp;</span><span>self</span><span> </span><span>{</span><span>message_count</span><span>,</span><span> </span><span>average_bytes</span><span>})</span><span> </span>-&gt; <span>usize</span> <span>{</span><span>
</span></span></span><span><span><span>        </span><span>//                    ----------------------------
</span></span></span><span><span><span>        </span><span>//             声明我们只读取这两个字段。
</span></span></span><span><span><span>        </span><span>self</span><span>.</span><span>message_count</span><span> </span><span>*</span><span> </span><span>self</span><span>.</span><span>average_bytes</span><span>
</span></span></span><span><span><span>    </span><span>}</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><h3 id="wont-it-be-limited-that-view-types-more-or-less-only-work-for-private-methods">视图类型几乎只适用于私有方法，这不会很受限吗？</h3>
<p>是的！但这是一个良好的起点。根据我的经验，这个问题<em>最常</em>出现在像我在这里展示的私有辅助方法中。它可能发生在公共上下文中，但频率要低得多，在那些情况下，重构类型以更好地向用户暴露分组通常更容易接受。这并不意味着我不想修复公共情况，只是意味着它是一个很好的用例，可以被排除在最小可行产品之外。将来，我将通过<a href="https://smallcultfollowing.com/babysteps/blog/2025/02/25/view-types-redux/" rel="noopener noreferrer">抽象字段</a>来解决公共字段，正如我过去所描述的那样。
</p><h3 id="what-if-i-am-borrowing-the-same-sets-of-fields-over-and-over-that-sounds-repititive">如果我一遍又一遍地借用同一组字段怎么办？这听起来很重复！</h3>
<p>确实如此！我将来希望看到某种“幽灵”或“抽象”字段，就像我在<a href="https://smallcultfollowing.com/babysteps/blog/2025/02/25/view-types-redux/" rel="noopener noreferrer">抽象字段</a>博客文章中描述的那样。但再次强调，这对我来说是“最小可行产品之后”的问题。
</p><h3 id="must-we-specify-the-field-sets-being-borrowed-explicitly-cant-they-be-inferred">我们必须显式指定被借用的字段集吗？它们不能被推断吗？</h3>
<p>在我描述的语法中，你必须显式编写 <code>&amp;mut place {field1, field2}</code>。但文献中有许多方法可以推断这种东西，其中<a href="https://en.wikipedia.org/wiki/Row_polymorphism" rel="noopener noreferrer">行多态</a>可能是最直接适用的。我认为我们绝对可以引入这种推断，事实上，我*可能会*让它成为默认值，这样 <code>&amp;mut place</code> *总是*引入一个视图类型，但在实践中通常被推断为“所有字段”。但这是对 Rust 推理系统的一个非平凡扩展，引入了一种我们今天不做的新推断。对于最小可行产品，我想我会依赖自动引用来涵盖绝大多数常见情况，而对其他情况使用显式语法。
</p><h3 id="man-i-have-to-write-the-fields-that-my-method-uses-in-the-signature-that-sucks-it-should-be-automatic">天哪，我必须在签名中写出我方法使用的字段？这太糟了！它应该是自动的！</h3>
<p>我理解对于许多应用，特别是*私有*方法，写出将要访问的字段列表似乎有点愚蠢：编译器应该能够弄清楚。</p>
<p>另一方面，这是我们在 Rust 中尽量避免的过程间推断，原因有很多：</p>
<ul>
<li>它引入了方法之间的依赖关系，使推断变得更加困难（在极端情况下甚至不可行）；</li>
<li>它导致“非本地错误”，用户会感到非常困惑，修改一个方法的主体会导致另一个方法出错（想想我们在 futures 和 <code>Send</code> 周围遇到的混乱）；</li>
<li>它使编译器更加复杂，我们将无法轻松地并行化（虽然我们今天没有并行化，但<a href="https://github.com/rust-lang/rust-project-goals/issues/121" rel="noopener noreferrer">这项工作正在进行中</a>！）</li>
</ul>
<p>对我来说，底线是<em>阶段性</em>：无论我们做什么，我认为我们都希望有一种方法来显式地确切知道哪些字段在哪里被访问。因此，我们应该首先添加这个。我们可以稍后再添加推断。
</p><h3 id="why-does-this-need-to-be-added-to-the-borrow-checker-why-not-desugar">为什么需要将其添加到借用检查器？为什么不脱糖？</h3>
<p>另一个常见的替代方案（也是我考虑了一段时间的……）是添加某种“脱糖”，传递字段的引用而不是单个引用。我不喜欢这个有两个原因。第一，我认为它实际上更复杂！这是对借用检查器的一个相当直接的更改，但那种脱糖会使代码遍布整个编译器，并且会使诊断等变得复杂得多。</p>
<p>但第二，它需要改变运行时发生的事情，我不明白为什么在这个例子中需要这样做。传递单个引用对我来说感觉是正确的。
</p><h3 id="what-about-the-ambiguous-grammar-what-other-syntax-options-are-there">关于模糊的语法怎么办？还有其他语法选项吗？</h3>
<p>哦，对了，模糊的语法。老实说，我还没有深入思考语法。我试图让类型 <code>Struct { field1, field 2 }</code> 反映结构体构造语法，因为我们通常试图让类型反映表达式，但这当然会导致借用表达式中的歧义，从而引起问题：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>foo</span><span> </span><span>=</span><span> </span><span>&amp;</span><span>mut</span><span> </span><span>some_variable</span><span> </span><span>{</span><span> </span><span>field1</span><span> </span><span>};</span><span>
</span></span></span><span><span><span>            </span><span>// ------------- 这是一个变量还是一个字段名？
</span></span></span></code></pre></div><p>我看到的选项：</p>
<ul>
<li><em>让它工作。</em> 它并非真正有歧义，但确实需要一些语义消歧，也就是说，在至少某些情况下，我们必须延迟解决这个问题，直到名称解析完成。这对 Rust 来说是不寻常的。我们在一些小的领域这样做，最著名的是对像 <code>None</code> 这样的模式的解释（它是绑定到变量 <code>None</code> 还是枚举变体？）。</li>
<li><em>仅为借用引入新语法。</em> 我们可以保留类型语法，但使借用语法不同，也许是 <code>&amp;mut {field1} in some_variable</code> 或其他形式。考虑到你很少会输入显式的借用形式，这看起来不错？</li>
<li><em>完全使用新语法。</em> 也许我们想尝试不同的东西，或者到处引入一个关键字？我很乐意在那里听取意见。当前的一个对我来说感觉不错，但它占据了“拥挤的语法空间”，所以我可以看出读者可能无法确定如何解释它，这可能会令人困惑。</li>
</ul>
<h2 id="conclusion-this-is-a-good-mvp-lets-ship-it">结论：这是一个很好的最小可行产品，让我们发布它吧！</h2>
<p>简而言之，我真的没有看到任何阻碍我们前进的东西，至少通过<a href="https://lang-team.rust-lang.org/how_to/experiment.html" rel="noopener noreferrer">语言实验</a>。</p><p><em>由 mimo-v2.5 模型翻译，花费 29562 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2026/03/21/view-types-max-min/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2026/03/21/view-types-max-min/</guid>
      <pubDate>Sat, 21 Mar 2026 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>极简视图类型，续篇</title>
      <category>view-types</category>
      <description>[AI 摘要] 这篇文章是关于 Rust 视图类型设计的后续讨论，探索了使用点语法和强制转换机制来改进结构体字段选择性借用的方案。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> 这篇文章是关于 Rust 视图类型设计的后续讨论，探索了使用点语法和强制转换机制来改进结构体字段选择性借用的方案。</div>一篇短文，用于记录我在上一篇文章中收到的两个有趣建议以及其他一些相关思考。
<h2 id="syntax-with-">使用 <code>.</code> 的语法</h2>
<p>有人通过邮件建议我可以使用 <code>.</code> 来消除语法歧义：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>place</span><span> </span><span>=</span><span> </span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>.{</span><span>statistics</span><span>};</span><span>
</span></span></span></code></pre></div><p>可以设想，我们也可以将此语法用于类型，例如：</p>
<div><pre><code><span><span><span>fn</span> <span>method</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>mp</span>: <span>&amp;</span><span>mut</span><span> </span><span>MessageProcessor</span><span>.{</span><span>statistics</span><span>},</span><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></pre></div><p>以及在 <code>self</code> 位置：</p>
<div><pre><code><span><span><span>fn</span> <span>foo</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>.{</span><span>statistics</span><span>})</span><span> </span><span>{}</span><span>
</span></span></span></code></pre></div><p>我需要花时间考虑一下，但……我有点喜欢它？</p>
<p>我会在下一个示例中使用它来试试看效果。</p>
<h2 id="coercion-for-calling-public-methods-that-name-private-types">调用引用私有类型的公共方法时的强制转换</h2>
<p>在我的文章中，我提到如果你有一个公共方法，其 <code>self</code> 类型引用了私有字段，你将无法从另一个作用域调用它：</p>
<div><pre><code><span><span><span>mod</span> <span>module</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>#[derive(Default)]</span><span>
</span></span></span><span><span><span>    </span><span>pub</span><span> </span><span>struct</span> <span>MessageProcessor</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>messages</span>: <span>Vec</span><span>&lt;</span><span>String</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>        </span><span>statistics</span>: <span>Statistics</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>}</span><span>
</span></span></span><span><span><span>    
</span></span></span><span><span><span>    </span><span>pub</span><span> </span><span>struct</span> <span>Statistics</span><span> </span><span>{</span><span> </span><span>..</span><span> </span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>    </span><span>impl</span><span> </span><span>MessageProcessor</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>pub</span><span> </span><span>fn</span> <span>push_message</span><span>(</span><span>
</span></span></span><span><span><span>            </span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>.{</span><span>messages</span><span>},</span><span>
</span></span></span><span><span><span>            </span><span>//         -------- 私有字段
</span></span></span><span><span><span>            </span><span>message</span>: <span>String</span><span>,</span><span>
</span></span></span><span><span><span>        </span><span>)</span><span> </span><span>{}</span><span>
</span></span></span><span><span><span>    </span><span>}</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>pub</span><span> </span><span>fn</span> <span>main</span><span>()</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>mp</span><span> </span><span>=</span><span> </span><span>MessageProcessor</span>::<span>default</span><span>();</span><span>
</span></span></span><span><span><span>    </span><span>mp</span><span>.</span><span>push_message</span><span>(</span><span>format!</span><span>(</span><span>"Hi"</span><span>));</span><span>
</span></span></span><span><span><span>    </span><span>// ------------ 错误！
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>错误源于将 <code>push_message</code> 脱糖为引用私有字段的调用：</p>
<div><pre><code><span><span><span>MessageProcessor</span>::<span>push_message</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>&amp;</span><span>mut</span><span> </span><span>mp</span><span>.{</span><span>messages</span><span>},</span><span>
</span></span></span><span><span><span>    </span><span>//       -------- 此处无法命名
</span></span></span><span><span><span>    </span><span>format!</span><span>(</span><span>"Hi"</span><span>),</span><span>
</span></span></span><span><span><span>)</span><span>
</span></span></span></code></pre></div><p>我提议我们可以通过 lint 来避免这种情况。</p>
<p>但有人提出了另一种方案：我们规定，当引入自动引用时，如果被调用者引用了从程序当前点不可见的局部变量，我们就借用整个结构体，而不是借用特定字段。</p>
<p>这样脱糖后的调用会变成：</p>
<div><pre><code><span><span><span>MessageProcessor</span>::<span>push_message</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>&amp;</span><span>mut</span><span> </span><span>mp</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>//   -- 借用整个结构体
</span></span></span><span><span><span>    </span><span>format!</span><span>(</span><span>"Hi"</span><span>),</span><span>
</span></span></span><span><span><span>)</span><span>
</span></span></span></code></pre></div><p>如果我们随后说 <code>&amp;mut MessageProcessor</code> 可以强制转换为 <code>&amp;mut MessageProcessor.{messages}</code>，那么这个调用就合法了。</p>
<p>有趣的是，自动解引用循环已经考虑了可见性：如果你做 <code>a.foo</code>，我们会解引用直到看到一个在你当前点可见的 <em><code>foo</code> 字段</em>。</p>
<h2 id="oh-and-a-side-note-assigning-etc">哦，顺便提一下，赋值等操作</h2>
<p>这引出了一个我之前没讨论过的有趣问题。当你为一个像 <code>MessageProcessor.{messages}</code> 这样的类型写入一个值时会发生什么？</p>
<p>例如，如果我这样做：</p>
<div><pre><code><span><span><span>fn</span> <span>swap_fields</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>mp1</span>: <span>&amp;</span><span>mut</span><span> </span><span>MessageProcessor</span><span>.{</span><span>messages</span><span>},</span><span>
</span></span></span><span><span><span>    </span><span>mp2</span>: <span>&amp;</span><span>mut</span><span> </span><span>MessageProcessor</span><span>.{</span><span>messages</span><span>},</span><span>
</span></span></span><span><span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>std</span>::<span>mem</span>::<span>swap</span><span>(</span><span>mp1</span><span>,</span><span> </span><span>mp2</span><span>);</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>我期望这会<em>仅</em>交换选定的字段（在这个例子中是 <code>messages</code>），而其他字段保持不变。</p>
<p>基本思想是，类型 <code>MessageProcessor.{messages}</code> 表示 messages 字段已初始化且可访问，而其他字段必须完全忽略。</p>
<h2 id="another-possible-future-extension-moved-values">另一个可能的未来扩展：移动的值</h2>
<p>这代表了另一个可能的未来扩展。今天，如果你从结构体中的一个字段移出，你就无法再作为整体使用该值：</p>
<div><pre><code><span><span><span>impl</span><span> </span><span>MessageProcessor</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>fn</span> <span>example</span><span>(</span><span>mut</span><span> </span><span>self</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>// 从 self.statistics 移出
</span></span></span><span><span><span>        </span><span>std</span>::<span>mem</span>::<span>drop</span><span>(</span><span>self</span><span>.</span><span>statistics</span><span>);</span><span>
</span></span></span><span><span><span>        
</span></span></span><span><span><span>        </span><span>// 现在我不能调用这个方法，
</span></span></span><span><span><span>        </span><span>// 因为我无法借用 `self`：
</span></span></span><span><span><span>        </span><span>self</span><span>.</span><span>push_message</span><span>(</span><span>format!</span><span>(</span><span>"Hi again"</span><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></pre></div><p>但有了选择性借用，我们可以允许这样做，你甚至可以返回“部分初始化”的值：</p>
<div><pre><code><span><span><span>impl</span><span> </span><span>MessageProcessor</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>fn</span> <span>take_statistics</span><span>(</span><span>
</span></span></span><span><span><span>        </span><span>mut</span><span> </span><span>self</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>)</span><span> </span>-&gt; <span>MessageProcessor</span><span>.{</span><span>messages</span><span>}</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>std</span>::<span>mem</span>::<span>drop</span><span>(</span><span>self</span><span>.</span><span>statistics</span><span>);</span><span>
</span></span></span><span><span><span>        </span><span>self</span><span>
</span></span></span><span><span><span>    </span><span>}</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>那会很巧妙。</p><p><em>由 mimo-v2.5 模型翻译，花费 10444 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2026/03/22/max-min-view-types-followup/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2026/03/22/max-min-view-types-followup/</guid>
      <pubDate>Sun, 22 Mar 2026 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>研讨会：面向社区的代理开发</title>
      <description>[AI 摘要] Symposium项目旨在让Rust crate作者通过提供扩展来改善社区的代理开发体验。</description>
      <content:encoded><![CDATA[<div style="background:#f0f4f8;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;margin:12px 0;font-size:14px;color:#555"><strong>[AI 摘要]</strong> Symposium项目旨在让Rust crate作者通过提供扩展来改善社区的代理开发体验。</div>我非常激动地宣布<a href="https://symposium.dev/blog/announcing-symposium.html" rel="noopener noreferrer"><em>Symposium</em>项目的首个版本</a>发布，并且它已<a href="https://rustfoundation.org/media/welcoming-symposium-to-the-rust-innovation-lab/" rel="noopener noreferrer">被纳入Rust基金会的创新实验室</a>。Symposium的目标是让Rust社区中的每个人都能参与改进代理开发。核心理念是crate作者除了代码之外，还应该能够提供技能、MCP服务器和其他扩展。然后，Symposium工具会根据你的依赖关系自动安装这些扩展。毕竟，谁能比维护crate的人更了解如何使用它呢？
<p>如果你想了解更多关于Symposium工作原理的细节，请参阅<a href="https://symposium.dev/blog/announcing-symposium.html" rel="noopener noreferrer">Jack Huey在Symposium主博客上发布的公告</a>。<em>本文</em>是我的配套文章，侧重于更个人化的内容——我本人致力于Symposium的原因。</p>
<h2 id="i-believe-in-extensibility-everywhere">我相信<em>无处不在的可扩展性</em></h2>
<p>简而言之，我相信<strong>无处不在的可扩展性</strong>。目前，Rust语言在可扩展性方面做得相当不错：得益于过程宏、trait和所有权，你可以编写Rust crate，提供感觉像是内置的新功能。但在其他工具方面，我们才刚刚开始提供可扩展性，我希望我们能加快脚步！</p>
<p>我希望crate作者能够提供自定义诊断信息。我希望他们能够提供自定义lint。我希望他们能够提供自定义优化。我希望他们能够提供自定义IDE重构。<strong>而且，当我开始尝试代理开发时，我也希望那里也有可扩展性。</strong></p>
<h2 id="symposium-puts-crate-authors-in-charge">Symposium让crate作者掌握主动权</h2>
<p>Symposium的目标是赋予crate作者以及更广泛的Rust社区直接影响使用代理编写Rust代码体验的能力。Rust是代理非常流行的目标语言，因为类型系统提供了强大的安全防护，并且能生成高效代码——而且<a href="https://smallcultfollowing.com/babysteps/blog/2025/07/31/rs-py-ts-trifecta/" rel="noopener noreferrer">我预测它只会变得更受欢迎</a>。</p>
<p>尽管Rust作为代理编码目标很受欢迎，但目前Rust社区在使用代理编写Rust代码的体验方面基本上是旁观者；我希望我们有一种方式能直接影响它。</p>
<p>Symposium应运而生。通过Symposium，Crate作者可以打包技能等，然后Symposium会自动将它们提供给你的代理。Symposium还负责弥合代理之间微小但真实存在的差距（例如，每个代理都有自己的钩子格式，有些使用<code>.agents/skills</code>，有些使用<code>.claude/skills</code>等）。</p>
<h2 id="example-the-assert-struct-crate">示例：assert-struct crate</h2>
<p>让我举个例子。考虑Carl Lerche最近创建的<a href="https://crates.io/crates/assert-struct" rel="noopener noreferrer">assert-truct</a> crate。<code>assert-struct</code>允许你编写方便的断言来测试特定结构体字段的值：</p>
<div><pre><code><span><span><span>assert_struct!</span><span>(</span><span>val</span><span>,</span><span> </span><span>_</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>items</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><span><span>    </span><span>tags</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>    </span><span>..</span><span>
</span></span></span><span><span><span>});</span><span>
</span></span></span></code></pre></div><h3 id="the-problem-agents-dont-know-about-it">问题：代理不知道它</h3>
<p>这个crate很不错，但当然，没有模型会知道如何使用它——它不在他们的训练集中。它们可以通过阅读文档弄清楚，但这会消耗更多token（昂贵、缓慢、消耗能源），所以这不是个好主意。</p>
<h3 id="you-could-teach-the-agent-how-to-use-it">你可以教代理如何使用它……</h3>
<p>实际上，人们<em>今天</em>做的是为他们的项目添加技能——例如，在他的<code>toasty</code> crate中，<a href="https://github.com/tokio-rs/toasty/blob/38f340dc64859b45486213936df1fec1edda3d11/.claude/skills/write-tests/SKILL.md#assert_struct-rule" rel="noopener noreferrer">Carl有一个测试技能，同时也展示了如何使用assert-struct</a>。但似乎每个<em>使用</em>该crate的人都重复这些内容是很愚蠢的。</p>
<h3 id="but-wouldnt-it-be-better-the-crate-could-teach-the-agent-itself">……但如果crate本身能教代理岂不是更好？</h3>
<p>有了Symposium，教你的代理如何使用依赖项应该不再是必要的。相反，你的crate可以发布自己的技能或其他扩展。</p>
<p>工作方式是：assert-struct crate在其自己的仓库中集中定义一次技能<sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup>。然后在<a href="https://github.com/symposium-dev/recommendations" rel="noopener noreferrer">Symposium的中央推荐仓库</a>中有一个单独的文件，指向assert-struct仓库。任何时候assert-struct仓库更新该技能，更新都会自动为你同步。太棒了！（你也可以直接将技能嵌入<a href="https://github.com/symposium-dev/recommendations" rel="noopener noreferrer">rr</a>仓库，但更新它们需要向该仓库提交PR。）</p>
<h2 id="frequently-asked-questions">常见问题</h2>
<h3 id="how-do-i-add-support-for-my-crate-to-symposium">如何为我的crate添加对Symposium的支持？</h3>
<p>很简单！查看这里的文档：</p>
<p><a href="https://symposium.dev/crate-authors/supporting-your-crate.html" rel="noopener noreferrer">https://symposium.dev/crate-authors/supporting-your-crate.html</a></p>
<h3 id="what-kind-of-extensions-does-symposium-support">Symposium支持哪种类型的扩展？</h3>
<p>目前支持技能、钩子和MCP服务器。</p>
<h3 id="why-does-symposium-have-a-centralized-repository">为什么Symposium有一个集中式仓库？</h3>
<p>目前我们允许技能<em>内容</em>以去中心化的方式定义，但要求将插件添加到我们的<a href="https://github.com/symposium-dev/recommendations" rel="noopener noreferrer">中央推荐仓库</a>。这是一个暂时的限制。我们最终期望允许crate作者以完全去中心化的方式添加技能和插件。</p>
<p>我们选择早期限制自己使用集中式仓库有三个原因：</p>
<ul>
<li>即使去中心化支持存在，集中式仓库也会很有用，因为总会有选择不提供该支持的crate。</li>
<li>拥有一个集中的插件列表将使我们在Symposium演进过程中更容易通知用户。</li>
<li>在寻找其他机制的同时，拥有集中式仓库将有助于防范恶意技能<sup id="fnref:2"><a href="#fn:2" rel="noopener noreferrer">2</a></sup>，因为我们可以审查添加的crate并轻松扫描其内容。</li>
</ul>
<h3 id="what-if-i-want-to-add-skills-for-crates-private-to-my-company-i-dont-want-to-put-those-in-the-central-repository">如果我想为公司内部的crate添加技能怎么办？我不想把<em>这些</em>放进中心仓库！</h3>
<p>没问题，你可以添加一个自定义插件源。</p>
<h3 id="are-you-aware-of-the-negative-externalities-of-llms">你意识到LLM的负面外部性了吗？</h3>
<p>我意识到了，非常清楚。我觉得我们今天看到的许多LLM用途并不好（例如，聊天机器人<a href="https://buttondown.com/apperceptive/archive/ai-is-bad-ux/" rel="noopener noreferrer">劫持对话和社交线索以赢得不应得的信任</a>），并且重新确认人们的偏见而不是挑战他们的想法。我担心<a href="https://nikomatsakis.github.io/rust-project-perspectives-on-ai/feb27-summary.html#ais-consume-a-lot-of-power" rel="noopener noreferrer">数据中心的环境成本以及公司如何背离了他们的气候目标</a>。我不喜欢<a href="https://nikomatsakis.github.io/rust-project-perspectives-on-ai/feb27-summary.html#ai-can-be-expensive-to-access-and-can-concentrate-power" rel="noopener noreferrer">集中式模型如何集中经济权力</a>。<sup id="fnref:3"><a href="#fn:3" rel="noopener noreferrer">3</a></sup>所以，是的，我看到了所有这些。同时，我也看到LLM如何让人们构建以前无法构建的东西，并帮助解决以前难以解决的问题——这包括越来越多从未想过自己是程序员的人<sup id="fnref:4"><a href="#fn:4" rel="noopener noreferrer">4</a></sup>。我通过Symposium和其他项目的目标是成为解决方案的一部分，寻找利用LLM产生净正面效应的方法：开启大门，而不是关闭它们。</p>
<h2 id="extensibility-because-everybody-has-something-to-offer">可扩展性：因为每个人都有贡献</h2>
<p>从根本上说，我致力于Symposium的原因是我相信<strong>每个人都有独特的东西可以贡献</strong>。我理解那些反映特定人物卓越愿景的、固执己见的系统的吸引力。但对我来说，最美丽的系统是每个人都能一起构建的系统<sup id="fnref:5"><a href="#fn:5" rel="noopener noreferrer">5</a></sup>。这就是为什么我喜欢开源。这就是为什么我喜欢emacs<sup id="fnref:6"><a href="#fn:6" rel="noopener noreferrer">6</a></sup>。这就是为什么我喜欢VSCode的扩展系统，它有很多很棒的宝石<sup id="fnref:7"><a href="#fn:7" rel="noopener noreferrer">7</a></sup>。</p>
<p>对我来说，Symposium在赋权方面是双赢的。首先，它使代理具有可扩展性，这将赋予crate作者更多支持其crate的能力。但它也有助于改善代理编程，<a href="https://smallcultfollowing.com/babysteps/blog/2025/02/10/love-the-llm/" rel="noopener noreferrer">我相信这最终将向更多人开放编程领域</a>。而这正是意义所在。</p>
<div>
<hr>
<ol>
<li id="fn:1">
<p>实际上，截至本文发布时，assert-struct技能直接嵌入在<a href="https://github.com/symposium-dev/recommendations" rel="noopener noreferrer">recommendations仓库</a>中。但我<a href="https://github.com/carllerche/assert-struct/pull/131" rel="noopener noreferrer">提了一个PR</a>将其放到assert-struct上，一旦合并我就会迁移过去。&nbsp;<a href="#fnref:1" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:2">
<p>这暂时限制了对技能内容的去中心化支持。我们最终期望允许crate作者以完全去中心化的方式添加技能和插件。&nbsp;<a href="#fnref:2" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:3">
<p>我非常有兴趣对开放模型做更多事情。&nbsp;<a href="#fnref:3" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:4">
<p>在亚马逊内部，看到许多从未想过自己是软件开发者的人开始构建软件，真是令人惊叹。考虑到软件行业在代表性方面的挑战，我觉得这非常令人鼓舞。<a href="https://www.forbes.com/sites/roncarucci/2024/01/24/one-more-time-why-diversity-leads-to-better-team-performance/" rel="noopener noreferrer">多元化的团队是更强大、更优秀的团队！</a>&nbsp;<a href="#fnref:4" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:5">
<p>这并不是说我不相信好的默认设置；这就是为什么我如今使用Zed和VSCode，而不是emacs，尽管我在概念上很喜欢它。&nbsp;<a href="#fnref:5" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:6">
<p>我的天哪。我大学时的一个朋友前段时间写了一篇关于emacs的<a href="https://waxbanks.wordpress.com/2025/08/01/bare-metal-the-emacs-essay/" rel="noopener noreferrer">精彩文章</a>。下次你在厕所里无休止地刷手机时，可以看看这篇文章。预先警告，它很长，所以你需要一些时间来读，但我认为它准确抓住了人们喜欢emacs的原因。&nbsp;<a href="#fnref:6" rel="noopener noreferrer">↩︎</a></p>
</li>
<li id="fn:7">
<p>这些天我真的很享受Zed，但我不得不说，我真的很想念<a href="https://github.com/kahole/edamagit" rel="noopener noreferrer">kahole/edamagit</a>！当然，它受到了<a href="https://github.com/magit/magit" rel="noopener noreferrer">magit emacs包</a>的启发。&nbsp;<a href="#fnref:7" rel="noopener noreferrer">↩︎</a></p>
</li>
</ol>
</div><p><em>由 mimo-v2.5 模型翻译，花费 7714 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2026/04/21/symposium/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2026/04/21/symposium/</guid>
      <pubDate>Tue, 21 Apr 2026 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>只需调用克隆（或别名）</title>
      <category>ergonomic-rc</category>
      <description>&lt;img src=&quot;https://smallcultfollowing.com/babysteps/
/assets/2025-justcallclone/keep-calm-and-call-clone-rendered.svg&quot; width=&quot;20%&quot; style=&quot;float: right; margin-right: 1em; margin-bottom: 0.5em;&quot; /&gt;
&lt;p&gt;在</description>
      <content:encoded><![CDATA[<img src="https://smallcultfollowing.com/babysteps/
/assets/2025-justcallclone/keep-calm-and-call-clone-rendered.svg" width="20%">
<p>在我关于人体工学引用计数的系列文章中，我想探讨另一个想法，我称之为“只需调用克隆（或别名）”。这个提案将特化 <code>clone</code> 和 <code>alias</code> 方法，使得在新的 Rust 版次中，编译器能够：（1）移除冗余或不必要的调用（通过 lint 警告）；以及（2）在需要时，自动在 <code>move</code> 闭包中捕获克隆或别名。</p>
<p>此提案的目标是简化用户的心理模型：每当遇到“使用已移动的值”之类的错误时，修复方法总是相同的：只需调用 <code>clone</code>（或 <code>alias</code>，如果适用）。该模型旨在达到我之前描述的<a href="https://smallcultfollowing.com/babysteps/
/blog/2025/10/13/ergonomic-explicit-handles/" rel="noopener noreferrer">“对内核来说足够底层，对图形用户界面来说足够易用”</a>的平衡。它也表明了一个观点：我们要保留的关键属性是<em>你总能发现新别名可能被创建的地方</em> – 但是，关于<em>确切何时</em>创建别名的细微差别是可以接受的。</p>

<h2 id="the-proposal-in-a-nutshell">提案概要</h2>
<h3 id="part-1-closure-desugaring-that-is-aware-of-clones-and-aliases">第一部分：感知克隆与别名的闭包脱糖</h3>
<p>考虑以下 <code>move</code> future：</p>
<div><pre><code><span><span><span>fn</span> <span>spawn_services</span><span>(</span><span>cx</span>: <span>&amp;</span><span>Context</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>tokio</span>::<span>task</span>::<span>spawn</span><span>(</span><span>async</span><span> </span><span>move</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>//                   ---- move future
</span></span></span><span><span><span>        </span><span>manage_io</span><span>(</span><span>cx</span><span>.</span><span>io_system</span><span>.</span><span>alias</span><span>(),</span><span> </span><span>cx</span><span>.</span><span>request_name</span><span>.</span><span>clone</span><span>());</span><span>
</span></span></span><span><span><span>        </span><span>//        --------------------  -----------------------
</span></span></span><span><span><span>    </span><span>});</span><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></pre></div><p>由于这是一个 <code>move</code> future，它会获取 <code>cx.io_system</code> 和 <code>cx_request_name</code> 的所有权。因为 <code>cx</code> 是一个借用引用，除非这些值是 <code>Copy</code> 的（它们显然不是），否则这将是一个错误。根据此提案，在 <code>move</code> 闭包/future 中捕获<em>别名</em>或<em>克隆</em>，将导致捕获该位置的一个<em>别名</em>或<em>克隆</em>。因此，这个 future 将会被脱糖为如下形式（使用<a href="https://smallcultfollowing.com/babysteps/
/blog/2025/10/22/explicit-capture-clauses/" rel="noopener noreferrer">显式捕获子句草案表示法</a>）：</p>
<div><pre><code><span><span><span>fn</span> <span>spawn_services</span><span>(</span><span>cx</span>: <span>&amp;</span><span>Context</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>tokio</span>::<span>task</span>::<span>spawn</span><span>(</span><span>
</span></span></span><span><span><span>        </span><span>async</span><span> </span><span>move</span><span>(</span><span>cx</span><span>.</span><span>io_system</span><span>.</span><span>alias</span><span>(),</span><span> </span><span>cx</span><span>.</span><span>request_name</span><span>.</span><span>clone</span><span>())</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>            </span><span>//     --------------------  -----------------------
</span></span></span><span><span><span>            </span><span>//     分别捕获别名/克隆
</span></span></span><span><span><span>
</span></span></span><span><span><span>            </span><span>manage_io</span><span>(</span><span>cx</span><span>.</span><span>io_system</span><span>.</span><span>alias</span><span>(),</span><span> </span><span>cx</span><span>.</span><span>request_name</span><span>.</span><span>clone</span><span>());</span><span>
</span></span></span><span><span><span>        </span><span>}</span><span>
</span></span></span><span><span><span>    </span><span>);</span><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></pre></div><h3 id="part-2-last-use-transformation">第二部分：最后使用转换</h3>
<p>然而，这个结果效率低下 – 现在存在<em>两个</em>别名/克隆。因此，提案的下一部分是，在新版 Rust 中，编译器将应用一种称为<strong>最后使用转换</strong>的新转换。此转换将识别不需要用于满足借用检查器的 <code>alias</code> 或 <code>clone</code> 调用，并将其移除。因此，这段代码将变为：</p>
<div><pre><code><span><span><span>fn</span> <span>spawn_services</span><span>(</span><span>cx</span>: <span>&amp;</span><span>Context</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>tokio</span>::<span>task</span>::<span>spawn</span><span>(</span><span>
</span></span></span><span><span><span>        </span><span>async</span><span> </span><span>move</span><span>(</span><span>cx</span><span>.</span><span>io_system</span><span>.</span><span>alias</span><span>(),</span><span> </span><span>cx</span><span>.</span><span>request_name</span><span>.</span><span>clone</span><span>())</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>            </span><span>manage_io</span><span>(</span><span>cx</span><span>.</span><span>io_system</span><span>,</span><span> </span><span>cx</span><span>.</span><span>request_name</span><span>);</span><span>
</span></span></span><span><span><span>            </span><span>//        ------------  ---------------
</span></span></span><span><span><span>            </span><span>//        转换为移动
</span></span></span><span><span><span>        </span><span>}</span><span>
</span></span></span><span><span><span>    </span><span>);</span><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></pre></div><p>最后使用转换将超越闭包范围。给定如下克隆 <code>id</code> 的例子，即使 <code>id</code> 之后从未被使用：</p>
<div><pre><code><span><span><span>fn</span> <span>send_process_identifier_request</span><span>(</span><span>id</span>: <span>String</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>request</span><span> </span><span>=</span><span> </span><span>Request</span>::<span>ProcessIdentifier</span><span>(</span><span>id</span><span>.</span><span>clone</span><span>());</span><span>
</span></span></span><span><span><span>    </span><span>//                                       ----------
</span></span></span><span><span><span>    </span><span>//                                       不必要
</span></span></span><span><span><span>    </span><span>send_request</span><span>(</span><span>request</span><span>)</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>用户将会收到如下警告<sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup>：</p>
<pre><code>warning: unnecessary `clone` call will be converted to a move
 --&gt; src/main.rs:7:40
  |
8 |     let request = Request::ProcessIdentifier(id.clone());
  |                                              ^^^^^^^^^^ unnecessary call to `clone`
  |
  = help: the compiler automatically removes calls to `clone` and `alias` when not
    required to satisfy the borrow checker
help: change `id.clone()` to `id` for greater clarity
  |
8 -     let request = Request::ProcessIdentifier(id.clone());
8 +     let request = Request::ProcessIdentifier(id);
  |
</code></pre><p>然后，代码将被转换为直接移动：</p>
<div><pre><code><span><span><span>fn</span> <span>send_process_identifier_request</span><span>(</span><span>id</span>: <span>String</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>request</span><span> </span><span>=</span><span> </span><span>Request</span>::<span>ProcessIdentifier</span><span>(</span><span>id</span><span>);</span><span>
</span></span></span><span><span><span>    </span><span>//                                       --
</span></span></span><span><span><span>    </span><span>//                                   已转换
</span></span></span><span><span><span>    </span><span>send_request</span><span>(</span><span>request</span><span>)</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><h2 id="mental-model-just-call-clone-or-alias">心理模型：只需调用 “clone”（或 “alias”）</h2>
<p>此提案的目标是，当你遇到“使用已移动的值”或“移动借用的内容”错误时，修复方法总是相同的：你只需调用 <code>clone</code>（或 <code>alias</code>）。无论该错误发生在常规函数体、闭包还是 future 中都无关紧要，编译器将插入所需的克隆/别名，以确保该位置的后续使用者可以访问它（且不会多于所需）。</p>
<p>我相信这将对新用户有所帮助。在 Rust 学习的早期，新用户经常在尝试建立牢固的心理模型时，或多或少随机地使用 <code>clone</code> 调用以及像 <code>&amp;</code> 这样的符号 – 这就是<a href="https://keepcalmandcallclone.website/" rel="noopener noreferrer">“保持冷静并调用克隆”</a>这个笑话的由来。目前，这种方法在闭包和 future 周围会失效。根据此提案，它将有效，但用户<em>也会</em>受益于指出不必要克隆的警告，我认为这将帮助他们理解 clone 真正<em>需要</em>在何处使用。</p>
<h2 id="experienced-users-can-trust-the-compiler-to-get-it-right">经验丰富的用户可以信任编译器能正确处理</h2>
<p>但真正的问题是这对<em>经验丰富的用户</em>来说效果如何。我一直在深入思考这个问题！我认为这种方法非常符合 Bjarne Stroustrup 对零成本抽象的经典定义：</p>
<blockquote>
<p>“你不使用的东西，你就不会为之付费。此外：你使用的东西，你手写代码也不会更好。”</p>
</blockquote>
<p>前半部分显然得到了满足。如果你不调用 <code>clone</code> 或 <code>alias</code>，此提案对你的生活没有影响。</p>
<p>关键在于后半部分：此提案的早期版本更为简单，有时会导致冗余或不必要的克隆和别名。经过反思，我认为这是一个不可行的起点。此提案要奏效，经验丰富的用户必须知道<strong>使用更显式的形式没有性能优势</strong>。这正是我们使用迭代器等方式的体验，我认为效果非常好。我相信此提案达到了这个目标，但我想听听是否有我遗漏的地方。</p>
<h2 id="the-last-use-transformation-codifies-a-widespread-intuition-that-clone-is-never-necessary">最后使用转换将一种普遍的直觉（即 <code>clone</code> 从来不是<em>必需</em>的）编纂为规则</h2>
<p>我认为大多数用户会预期，只要代码能继续编译，将 <code>message.clone()</code> 改为直接使用 <code>message</code> 是可以的。但事实上，没有任何东西<em>要求</em>必须如此。根据此提案，那些在特殊方式下使 <code>clone</code> 具有显著意义的 API，在新 Rust 版次中将更难使用，我预计它们最终会被修改，使得“有意义的克隆”有另一个名称。我认为这是一件好事。</p>
<h2 id="frequently-asked-questions">常见问题</h2>
<p>我想我已经涵盖了关键点。让我在这里通过 FAQ 深入探讨一些细节。</p>
<h3 id="can-you-summarize-all-of-these-posts-youve-been-writing-its-a-lot-to-digest">你能总结一下你写的所有这些文章吗？内容太多了，难以消化！</h3>
<p>我理解，我抛出了很多东西。让我先回顾一下我所看到的动机：</p>
<ul>
<li>我相信我们的目标应该是首先关注一种<a href="https://smallcultfollowing.com/babysteps/
/blog/2025/10/13/ergonomic-explicit-handles/" rel="noopener noreferrer">“对内核来说足够底层，对图形用户界面来说足够易用”</a>的设计。
<ul>
<li>这里的关键词是<em>足够</em>。我们需要确保底层细节被暴露出来，但仅限于那些真正重要的细节。并且我们需要确保它易于使用，但不必像 TypeScript 那样好（尽管那样很好）。</li>
</ul>
</li>
<li>Rust 当前的 <code>Clone</code> 方法对这两类用户都不够理想；
<ul>
<li><code>clone</code> 的调用对于内核和底层软件来说不够显式：当你看到 <code>something.clone()</code> 时，你不知道这会创建一个新的别名还是一个完全不同的值，你也不知道它在运行时的开销。社区中很多人推荐使用 <code>Arc::clone(&amp;something)</code> 而不是 <code>something.clone()</code> 是有原因的。</li>
<li><code>clone</code> 的调用，尤其是在闭包中，是一个<strong>主要的痛点</strong>，自从我们首次讨论这个问题以来，这已经成为一个明确的共识。</li>
</ul>
</li>
</ul>
<p>然后我提出了一系列三项更改来解决这些问题，每项更改都写在单独的博客文章中：</p>
<ul>
<li>首先，我们<a href="https://smallcultfollowing.com/babysteps/
/blog/2025/10/07/the-handle-trait/" rel="noopener noreferrer">引入了 <code>Alias</code> trait（最初称为 <code>Handle</code>）</a>。<code>Alias</code> trait 引入了一个新的 <code>alias</code> 方法，该方法等同于 <code>clone</code>，但表明这将创建相同底层值的第二个别名。</li>
<li>其次，我们引入了<a href="https://smallcultfollowing.com/babysteps/
/blog/2025/10/22/explicit-capture-clauses/" rel="noopener noreferrer">显式捕获子句</a>，它减轻了捕获克隆或别名的语法负担，使得可以预先声明闭包/future 捕获的全部值集合，并将支持其他方便的转换（例如，捕获 <code>as_ref</code> 或 <code>to_string</code> 的结果）。</li>
<li>最后，我们引入了本文中描述的<strong>只需调用 clone</strong> 提案。这修改了闭包脱糖以识别克隆/别名，并应用最后使用转换，在可能的情况下用移动替换克隆/别名调用。</li>
</ul>
<h3 id="what-would-it-feel-like-if-we-did-all-those-things">如果我们做了所有这些事情，感觉会如何？</h3>
<p>让我们通过一个例子（源自<a href="https://dioxus.notion.site/Dioxus-Labs-High-level-Rust-5fe1f1c9c8334815ad488410d948f05e" rel="noopener noreferrer">Dioxus 团队这篇精彩的博客文章</a>中的“Cloudflare 示例”）来探讨每组更改的影响：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>some_value</span><span> </span><span>=</span><span> </span><span>Arc</span>::<span>new</span><span>(</span><span>something</span><span>);</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>// 任务 1
</span></span></span><span><span><span>let</span><span> </span><span>_some_value</span><span> </span><span>=</span><span> </span><span>some_value</span><span>.</span><span>clone</span><span>();</span><span>
</span></span></span><span><span><span>tokio</span>::<span>task</span>::<span>spawn</span><span>(</span><span>async</span><span> </span><span>move</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>do_something_with</span><span>(</span><span>_some_value</span><span>);</span><span>
</span></span></span><span><span><span>});</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>// 任务 2：监听 DNS 连接
</span></span></span><span><span><span>let</span><span> </span><span>_some_a</span><span> </span><span>=</span><span> </span><span>self</span><span>.</span><span>some_a</span><span>.</span><span>clone</span><span>();</span><span>
</span></span></span><span><span><span>let</span><span> </span><span>_some_b</span><span> </span><span>=</span><span> </span><span>self</span><span>.</span><span>some_b</span><span>.</span><span>clone</span><span>();</span><span>
</span></span></span><span><span><span>let</span><span> </span><span>_some_c</span><span> </span><span>=</span><span> </span><span>self</span><span>.</span><span>some_c</span><span>.</span><span>clone</span><span>();</span><span>
</span></span></span><span><span><span>tokio</span>::<span>task</span>::<span>spawn</span><span>(</span><span>async</span><span> </span><span>move</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>  	</span><span>do_something_else_with</span><span>(</span><span>_some_a</span><span>,</span><span> </span><span>_some_b</span><span>,</span><span> </span><span>_some_c</span><span>)</span><span>
</span></span></span><span><span><span>});</span><span>
</span></span></span></code></pre></div><p>正如原始博客文章所言：</p>
<blockquote>
<p>在这个代码库上工作令人沮丧。我们想不出更好的架构方式 - 我们基本上需要所有东西的监听器，并且这些监听器需要根据应用状态过滤更新。你可能会说“好吧，提升技能”，但这个团队的工程师是我合作过的最聪明的人。Cloudflare 全力投入 Rust。他们愿意在这样的代码库上投入巨资。如果共享状态是这样工作的，那么核聚变问题不会用 Rust 来解决。</p>
</blockquote>
<p>应用 <a href="https://smallcultfollowing.com/babysteps/
/blog/2025/10/07/the-handle-trait/" rel="noopener noreferrer"><code>Alias</code> trait</a> 和 <a href="https://smallcultfollowing.com/babysteps/
/blog/2025/10/22/explicit-capture-clauses/" rel="noopener noreferrer">显式捕获子句</a> 会带来适度的改进。你现在可以清楚地看到对 <code>clone</code> 的调用实际上是 <code>alias</code> 调用，并且你不必再使用尴尬的 <code>_some_value</code> 和 <code>_some_a</code> 变量。但是，代码仍然相当冗长：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>some_value</span><span> </span><span>=</span><span> </span><span>Arc</span>::<span>new</span><span>(</span><span>something</span><span>);</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>// 任务 1
</span></span></span><span><span><span>tokio</span>::<span>task</span>::<span>spawn</span><span>(</span><span>async</span><span> </span><span>move</span><span>(</span><span>some_value</span><span>.</span><span>alias</span><span>())</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>do_something_with</span><span>(</span><span>some_value</span><span>);</span><span>
</span></span></span><span><span><span>});</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>// 任务 2：监听 DNS 连接
</span></span></span><span><span><span>tokio</span>::<span>task</span>::<span>spawn</span><span>(</span><span>async</span><span> </span><span>move</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>self</span><span>.</span><span>some_a</span><span>.</span><span>alias</span><span>(),</span><span>
</span></span></span><span><span><span>    </span><span>self</span><span>.</span><span>some_b</span><span>.</span><span>alias</span><span>(),</span><span>
</span></span></span><span><span><span>    </span><span>self</span><span>.</span><span>some_c</span><span>.</span><span>alias</span><span>(),</span><span>
</span></span></span><span><span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>  	</span><span>do_something_else_with</span><span>(</span><span>self</span><span>.</span><span>some_a</span><span>,</span><span> </span><span>self</span><span>.</span><span>some_b</span><span>,</span><span> </span><span>self</span><span>.</span><span>some_c</span><span>)</span><span>
</span></span></span><span><span><span>});</span><span>
</span></span></span></code></pre></div><p>应用“只需调用 Clone”提案可以消除大量样板代码，并且我认为它很好地捕捉了代码的<em>意图</em>。它还保留了相当多的显式性，因为搜索 <code>alias</code> 调用可以揭示所有将创建别名的地方。然而，它确实引入了一些细微差别，因为（例如）对 <code>self.some_a.alias()</code> 的调用实际上会在 future 被<em>创建时</em>发生，而不是在它被<em>执行时</em>：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>some_value</span><span> </span><span>=</span><span> </span><span>Arc</span>::<span>new</span><span>(</span><span>something</span><span>);</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>// 任务 1
</span></span></span><span><span><span>tokio</span>::<span>task</span>::<span>spawn</span><span>(</span><span>async</span><span> </span><span>move</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>do_something_with</span><span>(</span><span>some_value</span><span>.</span><span>alias</span><span>());</span><span>
</span></span></span><span><span><span>});</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>// 任务 2：监听 DNS 连接
</span></span></span><span><span><span>tokio</span>::<span>task</span>::<span>spawn</span><span>(</span><span>async</span><span> </span><span>move</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>  	</span><span>do_something_else_with</span><span>(</span><span>
</span></span></span><span><span><span>        </span><span>self</span><span>.</span><span>some_a</span><span>.</span><span>alias</span><span>(),</span><span>
</span></span></span><span><span><span>        </span><span>self</span><span>.</span><span>some_b</span><span>.</span><span>alias</span><span>(),</span><span>
</span></span></span><span><span><span>        </span><span>self</span><span>.</span><span>some_c</span><span>.</span><span>alias</span><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></pre></div><h3 id="im-worried-that-the-execution-order-of-calls-to-alias-will-be-too-subtle-how-is-thie-explicit-enough-for-low-level-code">我担心对 alias 的调用的执行顺序会太微妙。这如何做到“对底层代码足够显式”？</h3>
<p>毫无疑问，只需调用 Clone 使得闭包/future 脱糖更加微妙。查看任务 1：</p>
<div><pre><code><span><span><span>tokio</span>::<span>task</span>::<span>spawn</span><span>(</span><span>async</span><span> </span><span>move</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>do_something_with</span><span>(</span><span>some_value</span><span>.</span><span>alias</span><span>());</span><span>
</span></span></span><span><span><span>});</span><span>
</span></span></span></code></pre></div><p>这会被脱糖为在 future 被<em>创建时</em>调用 <code>alias</code>（而不是在它被<em>执行时</em>）。使用显式形式：</p>
<div><pre><code><span><span><span>tokio</span>::<span>task</span>::<span>spawn</span><span>(</span><span>async</span><span> </span><span>move</span><span>(</span><span>some_value</span><span>.</span><span>alias</span><span>())</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>do_something_with</span><span>(</span><span>some_value</span><span>)</span><span>
</span></span></span><span><span><span>});</span><span>
</span></span></span></code></pre></div><p>我完全可以想象人们一开始会感到困惑 – “但是那个 <code>alias</code> 调用看起来像是在 future（或闭包）内部，为什么它发生得更早？”</p>
<p><strong>然而，这段代码似乎确实保留了最重要的东西：</strong> 当我在代码库中搜索 <code>alias</code> 调用时，我会发现为此任务创建了一个别名。对于绝大多数现实世界的示例，别名是<em>在任务生成时</em>创建还是<em>在它执行时</em>创建的区分并不重要。看看这段代码：重要的是 <code>do_something_with</code> 是用 <code>some_value</code> 的别名调用的，所以只要 <code>do_something_else</code> 正在执行，<code>some_value</code> 就会保持存活。这些“管道”具体如何工作并不重要。</p>
<h3 id="what-about-futures-that-conditionally-alias-a-value">那些有条件地别名化值的 future 呢？</h3>
<p>是的，好问题，这类例子更容易引起混淆。像这个：</p>
<div><pre><code><span><span><span>tokio</span>::<span>task</span>::<span>spawn</span><span>(</span><span>async</span><span> </span><span>move</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>if</span><span> </span><span>false</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>do_something_with</span><span>(</span><span>some_value</span><span>.</span><span>alias</span><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></pre></div><p>在这个例子中，有代码使用 <code>some_value</code> 的别名，但仅在 <code>if false</code> 下。那么会发生什么？我推测 future 确实会捕获 <code>some_value</code> 的一个别名，就像这个 future 会<em>移动</em> <code>some_value</code> 一样，即使相关代码是死代码：</p>
<div><pre><code><span><span><span>tokio</span>::<span>task</span>::<span>spawn</span><span>(</span><span>async</span><span> </span><span>move</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>if</span><span> </span><span>false</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>do_something_with</span><span>(</span><span>some_value</span><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></pre></div><h3 id="can-you-give-more-details-about-the-closure-desugaring-you-imagine">你能提供更多关于你设想的闭包脱糖细节吗？</h3>
<p>可以！我想的大概是这样的：</p>
<ul>
<li>如果有<a href="https://smallcultfollowing.com/babysteps/
/blog/2025/10/22/explicit-capture-clauses/" rel="noopener noreferrer">显式捕获子句</a>，则使用它。
</li><li>否则：
<ul>
<li>对于非 <code>move</code> 闭包/future，没有变化，所以
<ul>
<li>分类每个位置的用法，并选择可用的“最弱选项”：
<ul>
<li>按引用
</li><li>按可变引用
</li><li>移动
</li></ul>
</li></ul>
</li></ul>
</li><li>对于 <code>move</code> 闭包/future，我们将改变
<ul>
<li>分类每个位置 <code>P</code> 的用法，并决定是否捕获该位置…
<ul>
<li><em>按克隆</em>：至少有一个调用 <code>P.clone()</code> 或 <code>P.alias()</code>，且 <code>P</code> 的所有其他用法只需要共享引用（读取）。
</li><li><em>按移动</em>：如果没有对 <code>P.clone()</code> 或 <code>P.alias()</code> 的调用，或者 <code>P</code> 的用法要求所有权或可变引用。
</li></ul>
</li><li>当位置 <code>a.b.c</code> 仅通过共享引用使用，并且其中至少一个是克隆或别名时，按克隆/别名捕获。
<ul>
<li>为此目的，访问“前缀位置” <code>a</code> 或“后缀位置” <code>a.b.c.d</code> 也被视为访问 <code>a.b.c</code>。
</li></ul>
</li></ul>
</li></ul>


<p>显示一些边缘情况的示例：</p>
<div><pre><code><span><span><span>if</span><span> </span><span>consume</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>x</span><span>.</span><span>foo</span><span>().</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><h3 id="why-not-do-something-similar-for-non-move-closures">为什么不对非 move 闭包做类似的事情？</h3>
<p>在相关情况下，非 move 闭包将已经通过共享引用来捕获。这意味着后续使用该变量通常会成功：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>f</span><span> </span><span>=</span><span> </span><span>async</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>//  ----- 不是 async move
</span></span></span><span><span><span>    </span><span>self</span><span>.</span><span>some_a</span><span>.</span><span>alias</span><span>()</span><span>
</span></span></span><span><span><span>};</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>do_something_else</span><span>(</span><span>self</span><span>.</span><span>some_a</span><span>.</span><span>alias</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>await</span><span>;</span><span>
</span></span></span></code></pre></div><p>这个 future 不需要获取 <code>self.some_a</code> 的所有权来创建别名，因此它只会捕获一个对 <code>self.some_a</code> 的<em>引用</em>。这意味着后续使用 <code>self.some_a</code> 仍然可以编译，没有问题。但是，如果这是一个 move 闭包，那么上面的代码目前将无法编译。</p>
<p>有一个边缘情况你可能会得到错误，那就是当你正在<em>移动</em>时：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>f</span><span> </span><span>=</span><span> </span><span>async</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>self</span><span>.</span><span>some_a</span><span>.</span><span>alias</span><span>()</span><span>
</span></span></span><span><span><span>};</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>do_something_else</span><span>(</span><span>self</span><span>.</span><span>some_a</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>await</span><span>;</span><span>
</span></span></span></code></pre></div><p>在那种情况下，你可以将其设为 <code>async move</code> 闭包和/或使用显式捕获子句：</p>
<h3 id="can-you-give-more-details-about-the-last-use-transformation-you-imagine">你能提供更多关于你设想的最后使用转换细节吗？</h3>
<p>可以！我们会在代码生成期间识别 <code>Clone::clone</code> 或 <code>Alias::alias</code> 的候选调用。在借用检查执行后，我们将检查每个调用点并检查借用检查信息以决定：</p>
<ul>
<li>这个位置以后会被访问吗？
</li><li>可能引用此位置的某些引用以后会被访问吗？
</li></ul>
<p>如果两个问题的答案都是“否”，那么我们将用原始位置的移动替换该调用。</p>
<p>这里是一些示例：</p>
<div><pre><code><span><span><span>fn</span> <span>borrow</span><span>(</span><span>message</span>: <span>Message</span><span>)</span><span> </span>-&gt; <span>String</span> <span>{</span><span>
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>method</span><span> </span><span>=</span><span> </span><span>message</span><span>.</span><span>method</span><span>.</span><span>to_string</span><span>();</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>    </span><span>send_message</span><span>(</span><span>message</span><span>.</span><span>clone</span><span>());</span><span>
</span></span></span><span><span><span>    </span><span>//           ---------------
</span></span></span><span><span><span>    </span><span>//           将被转换为
</span></span></span><span><span><span>    </span><span>//           直接使用 `message`
</span></span></span><span><span><span>
</span></span></span><span><span><span>    </span><span>method</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><div><pre><code><span><span><span>fn</span> <span>borrow</span><span>(</span><span>message</span>: <span>Message</span><span>)</span><span> </span>-&gt; <span>String</span> <span>{</span><span>
</span></span></span><span><span><span>    </span><span>send_message</span><span>(</span><span>message</span><span>.</span><span>clone</span><span>());</span><span>
</span></span></span><span><span><span>    </span><span>//           ---------------
</span></span></span><span><span><span>    </span><span>//           无法转换
</span></span></span><span><span><span>    </span><span>//           因为 `message.method` 在
</span></span></span><span><span><span>    </span><span>//           后面被引用了
</span></span></span><span><span><span>
</span></span></span><span><span><span>    </span><span>message</span><span>.</span><span>method</span><span>.</span><span>to_string</span><span>()</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><div><pre><code><span><span><span>fn</span> <span>borrow</span><span>(</span><span>message</span>: <span>Message</span><span>)</span><span> </span>-&gt; <span>String</span> <span>{</span><span>
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>r</span><span> </span><span>=</span><span> </span><span>&amp;</span><span>message</span><span>;</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>    </span><span>send_message</span><span>(</span><span>message</span><span>.</span><span>clone</span><span>());</span><span>
</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>//           `message` 且在后面被使用。
</span></span></span><span><span><span>
</span></span></span><span><span><span>    </span><span>r</span><span>.</span><span>method</span><span>.</span><span>to_string</span><span>()</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><h3 id="why-are-you-calling-it-the-last-use-transformation-and-not-optimization">为什么你称它为最后使用<em>转换</em>而不是<em>优化</em>？</h3>
<p>过去，我将最后使用<em>转换</em>称为<em>优化</em> – 但在这里我改变术语。这是因为，通常，<em>优化</em>应该对用户不可观察，除非通过执行时间的测量（或通过未定义行为），但这里显然不是这种情况。这种转换是编译器以确定性方式执行的机械转换。</p>
<h3 id="would-the-transformation-see-through-references">这种转换会“看透”引用吗？</h3>
<p>我认为是的，但是有限的。换句话说，我预计</p>
<div><pre><code><span><span><span>Clone</span>::<span>clone</span><span>(</span><span>&amp;</span><span>foo</span><span>)</span><span>
</span></span></span></code></pre></div><p>和</p>
<div><pre><code><span><span><span>let</span><span> </span><span>p</span><span> </span><span>=</span><span> </span><span>&amp;</span><span>foo</span><span>;</span><span>
</span></span></span><span><span><span>Clone</span>::<span>clone</span><span>(</span><span>p</span><span>)</span><span>
</span></span></span></code></pre></div><p>将以相同的方式进行转换（替换为 <code>foo</code>），并且对于更多中间使用级别也是如此。这将“源于”我设想的基于 MIR 的优化技术。不一定非要这样，我们可以对人们编写的语法更加挑剔，但我认为那会令人惊讶。</p>
<p>另一方面，你仍然可以用类似的方式欺骗它：</p>
<div><pre><code><span><span><span>fn</span> <span>identity</span><span>&lt;</span><span>T</span><span>&gt;</span><span>(</span><span>x</span>: <span>&amp;</span><span>T</span><span>)</span><span> </span>-&gt; <span>&amp;</span><span>T</span><span> </span><span>{</span><span> </span><span>x</span><span> </span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>identity</span><span>(</span><span>&amp;</span><span>foo</span><span>).</span><span>clone</span><span>()</span><span>
</span></span></span></code></pre></div><h3 id="would-the-transformation-apply-across-function-boundaries">这种转换会跨越函数边界吗？</h3>
<p>就我所想，不会。这种转换将局限于函数体内部。这意味着你可以编写一个像这样的 <code>force_clone</code> 方法，以某种方式“隐藏”克隆，使其永远不会被转换掉（这对于版次转换来说是一个重要的能力！）：</p>
<div><pre><code><span><span><span>fn</span> <span>pipe</span><span>&lt;</span><span>Msg</span>: <span>Clone</span><span>&gt;</span><span>(</span><span>message</span>: <span>Msg</span><span>)</span><span> </span>-&gt; <span>Msg</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>log</span><span>(</span><span>message</span><span>.</span><span>clone</span><span>());</span><span> </span><span>// &lt;-- 保留这个
</span></span></span><span><span><span>    </span><span>force_clone</span><span>(</span><span>&amp;</span><span>message</span><span>)</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p><em>由 mimo-v2.5 模型翻译，花费 37629 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2025/11/10/just-call-clone/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2025/11/10/just-call-clone/</guid>
      <pubDate>Mon, 10 Nov 2025 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>显式捕获子句</title>
      <category>ergonomic-rc</category>
      <description>&lt;p&gt;在我之前关于“符合人体工程学的引用计数”的文章中，我谈到无论我们做什么，都需要一种符合人体工程学的显式句柄创建方式。接下来的几篇文章将探讨我们可能实现这一点的几种方案。&lt;/p&gt;
&lt;p&gt;本文的重点是&lt;strong&gt;显式捕获子句&lt;/strong&gt;，它允许闭包使用一组显式指定的位置进行捕获注解。我认为显式捕获子句是一个不言而喻的好主意，原因我将在下文中阐述，我们绝对应该实现它；但它可能不足以被视为</description>
      <content:encoded><![CDATA[<p>在我之前关于“符合人体工程学的引用计数”的文章中，我谈到无论我们做什么，都需要一种符合人体工程学的显式句柄创建方式。接下来的几篇文章将探讨我们可能实现这一点的几种方案。</p>
<p>本文的重点是<strong>显式捕获子句</strong>，它允许闭包使用一组显式指定的位置进行捕获注解。我认为显式捕获子句是一个不言而喻的好主意，原因我将在下文中阐述，我们绝对应该实现它；但它可能不足以被视为<em>符合人体工程学</em>，因此我将在之后探索更多提案。</p>
<h2 id="motivation">动机</h2>
<p>Rust 闭包目前工作得很好，但我觉得有几个问题：</p>
<ul>
<li>教授和理解闭包脱糖是困难的，因为它缺乏一种显式形式。用户必须在脑海中进行脱糖才能理解发生了什么。</li>
<li>捕获一个值的“克隆”（或其他可能的转换）没有简洁的语法。</li>
<li>对于较长的闭包体，很难精确确定哪些值被捕获以及如何被捕获；你必须在闭包体中搜索对外部变量的引用，考虑遮蔽等。</li>
<li>很难形成关于何时需要 <code>move</code> 的直觉。我发现自己会在编译器要求时添加它，但这很烦人。</li>
</ul>
<h2 id="lets-look-at-a-strawperson-proposal">让我们看一个假设性提案</h2>
<p>不久前，我写了一个关于显式捕获子句的提案。实际上，我认为这个提案有很多缺陷，但我仍然要解释它：因为目前它是我知道的唯一一个具体的提案，而且它足以解释如何将显式捕获子句<em>视为</em>实现“显式<em>且</em>符合人体工程学”目标的一个方案。然后，我将介绍我喜欢这个提案的地方以及我不喜欢的地方。</p>
<h2 id="begin-with-move">从 <code>move</code> 开始</h2>
<p>该提案首先扩展了 <code>move</code> 关键字，允许其后带有一个要捕获的位置列表：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>closure</span><span> </span><span>=</span><span> </span><span>move</span><span>(</span><span>a</span><span>.</span><span>b</span><span>.</span><span>c</span><span>,</span><span> </span><span>x</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>do_something</span><span>(</span><span>a</span><span>.</span><span>b</span><span>.</span><span>c</span><span>.</span><span>d</span><span>,</span><span> </span><span>x</span><span>.</span><span>y</span><span>)</span><span>
</span></span></span><span><span><span>};</span><span>
</span></span></span></code></pre></div><p>然后闭包将拥有这两个位置的所有权；闭包体中对这些位置的引用将被替换为对这些捕获字段的访问。因此，上面的示例会脱糖为类似这样：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>closure</span><span> </span><span>=</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>struct</span> <span>MyClosure</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>a_b_c</span>: <span>Foo</span><span>,</span><span>
</span></span></span><span><span><span>        </span><span>x_y</span>: <span>Bar</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>    </span><span>impl</span><span> </span><span>FnOnce</span><span>&lt;</span><span>()</span><span>&gt;</span><span> </span><span>for</span><span> </span><span>MyClosure</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>fn</span> <span>call_once</span><span>(</span><span>self</span><span>)</span><span> </span>-&gt; <span>Baz</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>            </span><span>do_something</span><span>(</span><span>self</span><span>.</span><span>a_b_c</span><span>.</span><span>d</span><span>,</span><span> </span><span>self</span><span>.</span><span>x_y</span><span>)</span><span>
</span></span></span><span><span><span>            </span><span>//           ----------    --------
</span></span></span><span><span><span>            </span><span>//   位置 `a.b.c` 被重写为字段    |
</span></span></span><span><span><span>            </span><span>//   `self.a_b_c`              |
</span></span></span><span><span><span>            </span><span>//                  `x.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>MyClosure</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>a_b_c</span>: <span>self</span><span>.</span><span>a</span><span>.</span><span>b</span><span>.</span><span>c</span><span>,</span><span>
</span></span></span><span><span><span>        </span><span>x_y</span>: <span>self</span><span>.</span><span>x</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></code></pre></div><p>当使用这种简单的列表时，尝试引用未被捕获的其他位置将导致错误：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>closure</span><span> </span><span>=</span><span> </span><span>move</span><span>(</span><span>a</span><span>.</span><span>b</span><span>.</span><span>c</span><span>,</span><span> </span><span>x</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>do_something</span><span>(</span><span>a</span><span>.</span><span>b</span><span>.</span><span>c</span><span>.</span><span>d</span><span>,</span><span> </span><span>x</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>//           OK       错误: `x.z` 未被捕获
</span></span></span><span><span><span>};</span><span>
</span></span></span></code></pre></div><h2 id="capturing-with-rewrites">带重写的捕获</h2>
<p>也可以使用 <code>=</code> 号来捕获一个自定义表达式。例如，你可以将上述闭包重写如下：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>closure</span><span> </span><span>=</span><span> </span><span>move</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>a</span><span>.</span><span>b</span><span>.</span><span>c</span><span> </span><span>=</span><span> </span><span>a</span><span>.</span><span>b</span><span>.</span><span>c</span><span>.</span><span>clone</span><span>(),</span><span>
</span></span></span><span><span><span>    </span><span>x</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>do_something</span><span>(</span><span>a</span><span>.</span><span>b</span><span>.</span><span>c</span><span>.</span><span>d</span><span>,</span><span> </span><span>x</span><span>.</span><span>z</span><span>)</span><span>
</span></span></span><span><span><span>};</span><span>
</span></span></span></code></pre></div><p>它将脱糖为：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>closure</span><span> </span><span>=</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>struct</span> <span>MyClosure</span><span> </span><span>{</span><span> </span><span>/* 如上 */</span><span> </span><span>}</span><span>
</span></span></span><span><span><span>    </span><span>impl</span><span> </span><span>FnOnce</span><span>&lt;</span><span>()</span><span>&gt;</span><span> </span><span>for</span><span> </span><span>MyClosure</span><span> </span><span>{</span><span> </span><span>/* 如上 */</span><span> </span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>    </span><span>MyClosure</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>a_b_c</span>: <span>self</span><span>.</span><span>a</span><span>.</span><span>b</span><span>.</span><span>c</span><span>.</span><span>clone</span><span>(),</span><span>
</span></span></span><span><span><span>        </span><span>//     ------------------
</span></span></span><span><span><span>        </span><span>x_y</span>: <span>self</span><span>.</span><span>x</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></code></pre></div><p>当使用这种形式时，赋值给 <code>a.b.c</code> 的表达式必须与周围作用域中的 <code>a.b.c</code> 具有相同的类型。因此，这将是错误的：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>closure</span><span> </span><span>=</span><span> </span><span>move</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>a</span><span>.</span><span>b</span><span>.</span><span>c</span><span> </span><span>=</span><span> </span><span>22</span><span>,</span><span> </span><span>// 错误: `i32` 不是 `Foo`
</span></span></span><span><span><span>    </span><span>x</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></code></pre></div><h2 id="shorthands-and-capturing-by-reference">简写和按引用捕获</h2>
<p>你可以将 <code>move(a.b)</code> 理解为 <code>move(a.b = a.b)</code> 的语法糖。我们还支持其他方便的简写，例如：</p>
<div><pre><code><span><span><span>move</span><span>(</span><span>a</span><span>.</span><span>b</span><span>.</span><span>clone</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>move</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><span>clone</span><span>())</span><span> </span><span>||</span><span> </span><span>{</span><span>..</span><span>.}</span><span>
</span></span></span></code></pre></div><p>还有两个比较特殊的简写：</p>
<div><pre><code><span><span><span>move</span><span>(</span><span>&amp;</span><span>a</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>move</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>a</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></code></pre></div><p>它们很特殊，因为捕获的值确实是 <code>&amp;a.b</code> 和 <code>&amp;mut a.b</code>——但这本身不起作用，因为类型不匹配。因此，我们将每个对 <code>a.b</code> 的访问重写为对 <code>a_b</code> 字段的解引用，例如 <code>*self.a_b</code>：</p>
<div><pre><code><span><span><span>move</span><span>(</span><span>&amp;</span><span>a</span><span>.</span><span>b</span><span>)</span><span> </span><span>||</span><span> </span><span>{</span><span> </span><span>foo</span><span>(</span><span>a</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>struct</span> <span>MyStruct</span><span>&lt;</span><span>'l</span><span>&gt;</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>a_b</span>: <span>&amp;</span><span>'l</span> <span>Foo</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>impl</span><span> </span><span>FnOnce</span><span> </span><span>for</span><span> </span><span>MyStruct</span><span>&lt;</span><span>'_</span><span>&gt;</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>fn</span> <span>call_once</span><span>(</span><span>self</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>foo</span><span>(</span><span>*</span><span>self</span><span>.</span><span>a_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>MyStruct</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>a_b</span>: <span>&amp;</span><span>a</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>move</span><span>(</span><span>&amp;</span><span>a</span><span>.</span><span>b</span><span>)</span><span> </span><span>||</span><span> </span><span>{</span><span> </span><span>foo</span><span>(</span><span>*</span><span>a</span><span>.</span><span>b</span><span>)</span><span> </span><span>}</span><span>
</span></span></span></code></pre></div><p>这种转换有很多先例：这正是我们为 <code>Deref</code> trait 和现有的闭包捕获所做的。</p>
<h2 id="fresh-variables">新变量</h2>
<p>我们还应该允许你定义新变量。它们可以具有任意类型。这些值在闭包创建时求值，并存储在闭包元数据中：</p>
<div><pre><code><span><span><span>move</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>data</span><span> </span><span>=</span><span> </span><span>load_data</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>take</span><span>(</span><span>&amp;</span><span>data</span><span>,</span><span> </span><span>y</span><span>)</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><h2 id="open-ended-captures">开放式捕获</h2>
<p>到目前为止，我们所有的示例都完整列举了捕获的变量。但 Rust 闭包目前会根据使用的路径来推断捕获集（以及捕获方式）。我们也应该允许这样做。我会使用一个 <code>..</code> 糖语法来允许这样做，这样这两个闭包是等价的：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>c2</span><span> </span><span>=</span><span> </span><span>move</span><span> </span><span>||</span><span> </span><span>/* 闭包 */</span><span>;</span><span>
</span></span></span><span><span><span>//       ---- 捕获任何被使用的东西，
</span></span></span><span><span><span>//            获取所有权
</span></span></span><span><span><span>
</span></span></span><span><span><span>let</span><span> </span><span>c1</span><span> </span><span>=</span><span> </span><span>move</span><span>(</span><span>..</span><span>)</span><span> </span><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></pre></div><p>当然，你可以组合使用：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>c</span><span> </span><span>=</span><span> </span><span>move</span><span>(</span><span>x</span><span>.</span><span>y</span><span>.</span><span>clone</span><span>(),</span><span> </span><span>..</span><span>)</span><span> </span><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></pre></div><p>你也可以写 <code>ref</code> 来获得等同于 <code>||</code> 闭包的效果：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>c2</span><span> </span><span>=</span><span> </span><span>||</span><span> </span><span>/* 闭包 */</span><span>;</span><span>
</span></span></span><span><span><span>//       -- 捕获任何被使用的东西，
</span></span></span><span><span><span>//          如果可能的话使用引用
</span></span></span><span><span><span>let</span><span> </span><span>c1</span><span> </span><span>=</span><span> </span><span>move</span><span>(</span><span>ref</span><span>)</span><span> </span><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></pre></div><p>这让你可以这样写：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>c</span><span> </span><span>=</span><span> </span><span>move</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>a</span><span>.</span><span>b</span><span>.</span><span>clone</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>ref</span><span>
</span></span></span><span><span><span>)</span><span> </span><span>||</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>combine</span><span>(</span><span>&amp;</span><span>a</span><span>.</span><span>b</span><span>,</span><span> </span><span>&amp;</span><span>c</span><span>,</span><span> </span><span>&amp;</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><span>    </span><span>//        |    |
</span></span></span><span><span><span>    </span><span>//        |   这个将被按值捕获
</span></span></span><span><span><span>    </span><span>//        |   因为它是被显式命名的。
</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.clone()`
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><h2 id="frequently-asked-questions">常见问题</h2>
<h3 id="how-does-this-help-with-our-motivation">这如何帮助我们实现动机？</h3>
<p>让我们看看我提出的动机：</p>
<h4 id="teaching-and-understanding-closure-desugaring-is-difficult">教授和理解闭包脱糖是困难的</h4>
<p>虽然有很多语法，但它也为你提供了一种显式形式，你可以用它来进行解释。要理解我的意思，请考虑这两个闭包之间的区别（<a href="" rel="noopener noreferrer">playground</a>）。</p>
<p>第一个闭包使用 <code>||</code>：</p>
<div><pre><code><span><span><span>fn</span> <span>main</span><span>()</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>3</span><span>;</span><span>
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>c_attached</span><span> </span><span>=</span><span> </span><span>||</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>let</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>i</span><span> </span><span>+</span><span> </span><span>1</span><span>;</span><span>
</span></span></span><span><span><span>        </span><span>std</span>::<span>mem</span>::<span>replace</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>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></span></span></code></pre></div><p>而第二个闭包使用 <code>move</code>：</p>
<div><pre><code><span><span><span>fn</span> <span>main</span><span>()</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>3</span><span>;</span><span>
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>mut</span><span> </span><span>c_detached</span><span> </span><span>=</span><span> </span><span>move</span><span> </span><span>||</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>let</span><span> </span><span>j</span><span> </span><span>=</span><span> </span><span>i</span><span> </span><span>+</span><span> </span><span>1</span><span>;</span><span>
</span></span></span><span><span><span>        </span><span>std</span>::<span>mem</span>::<span>replace</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>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></code></pre></div><p>它们实际上非常不同，<a href="https://play.rust-lang.org/?version=stable&amp;mode=debug&amp;edition=2024&amp;gist=fec374e4055a99aa3dda9e66a5c03495" rel="noopener noreferrer">正如你在这个 playground 中看到的</a>。但为什么？嗯，第一个闭包脱糖为捕获一个引用：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>mut</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>3</span><span>;</span><span>
</span></span></span><span><span><span>let</span><span> </span><span>mut</span><span> </span><span>c_attached</span><span> </span><span>=</span><span> </span><span>move</span><span>(</span><span>&amp;</span><span>i</span><span>)</span><span> </span><span>||</span><span> </span><span>{</span><span>..</span><span>.};</span><span>
</span></span></span></code></pre></div><p>而第二个按值捕获：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>mut</span><span> </span><span>i</span><span> </span><span>=</span><span> </span><span>3</span><span>;</span><span>
</span></span></span><span><span><span>let</span><span> </span><span>mut</span><span> </span><span>c_attached</span><span> </span><span>=</span><span> </span><span>move</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></pre></div><p>以前，要解释这一点，我不得不借助于脱糖为结构体。</p>
<h4 id="capturing-a-clone-is-painful">捕获克隆是痛苦的</h4>
<p>如果你有一个闭包想要捕获某物的克隆，今天你必须引入一个新变量。所以像这样的东西：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>closure</span><span> </span><span>=</span><span> </span><span>move</span><span> </span><span>||</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>begin_actor</span><span>(</span><span>data</span><span>,</span><span> </span><span>self</span><span>.</span><span>tx</span><span>.</span><span>clone</span><span>())</span><span>
</span></span></span><span><span><span>};</span><span>
</span></span></span></code></pre></div><p>会变成</p>
<div><pre><code><span><span><span>let</span><span> </span><span>closure</span><span> </span><span>=</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>self_tx</span><span> </span><span>=</span><span> </span><span>self</span><span>.</span><span>tx</span><span>.</span><span>clone</span><span>();</span><span>
</span></span></span><span><span><span>    </span><span>move</span><span> </span><span>||</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>begin_actor</span><span>(</span><span>data</span><span>,</span><span> </span><span>self_tx</span><span>.</span><span>clone</span><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></pre></div><p>这很笨拙。在这个提案下，可以逐点替换特定项：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>closure</span><span> </span><span>=</span><span> </span><span>move</span><span>(</span><span>self</span><span>.</span><span>tx</span><span>.</span><span>clone</span><span>(),</span><span> </span><span>..</span><span>)</span><span> </span><span>||</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>begin_actor</span><span>(</span><span>data</span><span>,</span><span> </span><span>self</span><span>.</span><span>tx</span><span>.</span><span>clone</span><span>())</span><span>
</span></span></span><span><span><span>};</span><span>
</span></span></span></code></pre></div><h4 id="for-long-closure-bodies-it-is-hard-to-determine-precisely-which-values-are-captured-and-how">对于较长的闭包体，很难精确确定哪些值被捕获以及如何被捕获</h4>
<p>快！这个闭包使用了环境中的哪些变量？</p>
<div><pre><code><span><span><span>.</span><span>flat_map</span><span>(</span><span>move</span><span> </span><span>|</span><span>(</span><span>severity</span><span>,</span><span> </span><span>lints</span><span>)</span><span>|</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>parse_tt_as_comma_sep_paths</span><span>(</span><span>lints</span><span>,</span><span> </span><span>edition</span><span>)</span><span>
</span></span></span><span><span><span>    </span><span>.</span><span>into_iter</span><span>()</span><span>
</span></span></span><span><span><span>    </span><span>.</span><span>flat_map</span><span>(</span><span>move</span><span> </span><span>|</span><span>lints</span><span>|</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>// 用 `::` 重新连接标识符，这样它们之间就没有空格。
</span></span></span><span><span><span>        </span><span>lints</span><span>.</span><span>into_iter</span><span>().</span><span>map</span><span>(</span><span>move</span><span> </span><span>|</span><span>lint</span><span>|</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>            </span><span>(</span><span>
</span></span></span><span><span><span>                </span><span>lint</span><span>.</span><span>segments</span><span>().</span><span>filter_map</span><span>(</span><span>
</span></span></span><span><span><span>                    </span><span>|</span><span>segment</span><span>|</span><span> </span><span>segment</span><span>.</span><span>name_ref</span><span>()</span><span>
</span></span></span><span><span><span>                </span><span>).</span><span>join</span><span>(</span><span>"::"</span><span>).</span><span>into</span><span>(),</span><span>
</span></span></span><span><span><span>                </span><span>severity</span><span>,</span><span>
</span></span></span><span><span><span>            </span><span>)</span><span>
</span></span></span><span><span><span>        </span><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></pre></div><p>不知道？我也是。那这个呢？</p>
<div><pre><code><span><span><span>.</span><span>flat_map</span><span>(</span><span>move</span><span>(</span><span>edition</span><span>)</span><span> </span><span>|</span><span>(</span><span>severity</span><span>,</span><span> </span><span>lints</span><span>)</span><span>|</span><span> </span><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></pre></div><p>啊，就很清楚了！我发现一旦一个闭包超过几行，它就会使函数变得难以阅读，因为它很难判断它可能访问哪些变量。我遇到过一些函数，出于正确性原因，某个特定的闭包只能访问其周围值的一个子集，这很重要，但我现在没有办法表明这一点。有时我会创建单独的函数，但如果我能显式注解闭包的捕获方式，那就更好了。</p>
<h4 id="it-is-hard-to-develop-an-intuition-for-when-move-is-required">很难形成关于何时需要 <code>move</code> 的直觉</h4>
<p>嗯，实际上，我认为这种表示法对此毫无帮助！下面会详细说明。</p>
<p>让我介绍一些你可能对这个设计提出的问题。</p>
<h3 id="why-allow-the-capture-clause-to-specify-an-entire-place-like-abc">为什么允许“捕获子句”指定整个位置，如 <code>a.b.c</code>？</h3>
<p>目前你可以编写捕获位置的闭包，例如下面的 <code>self.context</code>：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>closure</span><span> </span><span>=</span><span> </span><span>move</span><span> </span><span>||</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>send_data</span><span>(</span><span>self</span><span>.</span><span>context</span><span>,</span><span> </span><span>self</span><span>.</span><span>other_field</span><span>)</span><span>
</span></span></span><span><span><span>};</span><span>
</span></span></span></code></pre></div><p>我的目标是能够获取这样一个闭包，并添加注解来改变特定位置的捕获方式，而无需对闭包体进行深度重写：</p>
<div><pre><code><span><span><span>let</span><span> </span><span>closure</span><span> </span><span>=</span><span> </span><span>move</span><span>(</span><span>self</span><span>.</span><span>context</span><span>.</span><span>clone</span><span>(),</span><span> </span><span>..</span><span>)</span><span> </span><span>||</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>//            --------------------------
</span></span></span><span><span><span>    </span><span>//            唯一的改变
</span></span></span><span><span><span>    </span><span>send_data</span><span>(</span><span>self</span><span>.</span><span>context</span><span>,</span><span> </span><span>self</span><span>.</span><span>other_field</span><span>)</span><span>
</span></span></span><span><span><span>};</span><span>
</span></span></span></code></pre>&lt;/</div><p><em>由 mimo-v2.5 模型翻译，花费 34112 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2025/10/22/explicit-capture-clauses/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2025/10/22/explicit-capture-clauses/</guid>
      <pubDate>Wed, 22 Oct 2025 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>动态异步trait，第十部分：Box box box</title>
      <category>dyn-async-traits</category>
      <description>&lt;p&gt;本文略偏离了我的&lt;a href=&quot;https://smallcultfollowing.com/babysteps/
/series/rust-in-2025/&quot;&gt;Rust 2025系列&lt;/a&gt;。我想分享我关于如何支持具有异步函数的trait的&lt;code&gt;dyn Trait&lt;/code&gt;的最新思考，特别是如何以与&lt;a href=&quot;https://smallcultfollowing.com/</description>
      <content:encoded><![CDATA[<p>本文略偏离了我的<a href="https://smallcultfollowing.com/babysteps/
/series/rust-in-2025/" rel="noopener noreferrer">Rust 2025系列</a>。我想分享我关于如何支持具有异步函数的trait的<code>dyn Trait</code>的最新思考，特别是如何以与<a href="https://smallcultfollowing.com/babysteps/
/blog/2022/09/18/dyn-async-traits-part-8-the-soul-of-rust/" rel="noopener noreferrer">Rust的灵魂</a>兼容的方式来实现。</p>
<h2 id="background-why-is-this-hard">背景：为什么这很困难？</h2>
<p>在动态trait中支持<code>async fn</code>是一个棘手的平衡艺术。挑战在于调和人们喜爱Rust的两个关键特性：其表达高层次、高效代码的能力，<em>以及</em>其专注于揭示底层细节。当涉及到trait中的异步函数时，这两者直接冲突，正如我在<a href="https://smallcultfollowing.com/babysteps/
/blog/2021/09/30/dyn-async-traits-part-1/" rel="noopener noreferrer">本系列的第一篇博客文章</a>中解释的那样——那几乎是四年前写的了！（天哪。）</p>
<p>要理解这个挑战，请看这个<code>Signal</code> trait示例：</p>
<div><pre><code><span><span><span>trait</span><span> </span><span>Signal</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>async</span><span> </span><span>fn</span> <span>signal</span><span>(</span><span>&amp;</span><span>self</span><span>);</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>在当前的Rust中，你可以编写一个接受<code>impl Signal</code>并调用<code>signal</code>的函数，一切都感觉相当不错：</p>
<div><pre><code><span><span><span>async</span><span> </span><span>fn</span> <span>send_signal_1</span><span>(</span><span>impl_trait</span>: <span>&amp;</span><span>impl</span><span> </span><span>Signal</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>impl_trait</span><span>.</span><span>signal</span><span>().</span><span>await</span><span>;</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>但我想要用<code>dyn Signal</code>编写同一个函数。如果我这样写……</p>
<div><pre><code><span><span><span>async</span><span> </span><span>fn</span> <span>send_signal_2</span><span>(</span><span>dyn_trait</span>: <span>&amp;</span><span>dyn</span><span> </span><span>Signal</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>dyn_trait</span><span>.</span><span>signal</span><span>().</span><span>await</span><span>;</span><span> </span><span>//   ---------- 错误
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>……我会得到一个错误。为什么呢？答案是编译器需要知道<code>signal</code>将返回什么样的future，以便可以对其进行await。至少它需要知道那个future有多大，以便为它分配空间<sup id="fnref:1"><a href="#fn:1" rel="noopener noreferrer">1</a></sup>。使用<code>impl Signal</code>时，编译器确切知道你有什么类型的signal，所以没问题：但使用<code>dyn Signal</code>时，我们不知道，因此就卡住了。</p>
<p>这个问题最常见的解决方案是将返回的future进行<em>装箱</em>。例如，<a href="https://crates.io/crates/async-trait" rel="noopener noreferrer"><code>async-trait</code> crate</a>将<code>async fn signal(&amp;self)</code>转换为类似<code>fn signal(&amp;self) -&gt; Box&lt;dyn Future&lt;Output = ()&gt; + '_&gt;</code>的东西。但在trait级别这样做意味着即使你使用<code>impl Trait</code>也会增加开销；它也排除了Rust异步的一些应用，如嵌入式或内核开发。</p>
<p>所以游戏的名称是找到让人们以方便<em>且</em>灵活的方式使用<code>dyn Trait</code>的方法。这被证明是相当困难的！</p>
<h2 id="the-box-box-box-design-in-a-nutshell">“box box box”设计概要</h2>
<p>最近我与<a href="https://github.com/compiler-errors" rel="noopener noreferrer">Michal Goulet（又名compiler-errors）</a>进行了一系列对话，重新深入探讨了这个问题，这让我思考了一种称为“box box box”的新方法。</p>
<p>“box box box”设计始于<a href="https://smallcultfollowing.com/babysteps/
/blog/2022/09/21/dyn-async-traits-part-9-callee-site-selection/" rel="noopener noreferrer">调用点选择</a>方法。在这种方法中，当你调用<code>dyn_trait.signal()</code>时，你得到的类型是<code>dyn Future</code>——即一个未固定大小的值。这不能直接使用。相反，你必须为它分配存储。最简单和最常见的方法是使用新的<code>.box</code>操作符将其装箱：</p>
<div><pre><code><span><span><span>async</span><span> </span><span>fn</span> <span>send_signal_2</span><span>(</span><span>dyn_trait</span>: <span>&amp;</span><span>dyn</span><span> </span><span>Signal</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>dyn_trait</span><span>.</span><span>signal</span><span>().</span><span>box</span><span>.</span><span>await</span><span>;</span><span>
</span></span></span><span><span><span>    </span><span>//        ------------
</span></span></span><span><span><span>    </span><span>// 结果是一个 `Box&lt;dyn Future&lt;Output = ()&gt;&gt;`。
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>这种方法相当容易解释。当你通过<code>dyn Trait</code>调用异步函数时，它会产生一个<code>dyn Future</code>，在你能够使用它之前必须存储在某个地方。最简单的选项是使用<code>.box</code>操作符将其存储在一个装箱中；这会给你一个<code>Box&lt;dyn Future&gt;</code>，你可以对其进行await。</p>
<p>但这个简单的解释掩盖了Rust的两个相当根本的变化。首先，它改变了<code>Trait</code>和<code>dyn Trait</code>的关系。其次，它引入了这个<code>.box</code>操作符，这将是<code>box</code>关键字<sup id="fnref:2"><a href="#fn:2" rel="noopener noreferrer">2</a></sup>的首次稳定使用——只为了这一种用途而引入这个关键字似乎很奇怪。</p>
<p>事实上，我认为这两个根本性的变化可能都是非常好的事情。本文的重点是解释它们打开了什么大门，以及它们可能将我们带向何处。</p>
<h2 id="change-0-unsized-return-value-methods">变化0：未固定大小返回值方法</h2>
<p>让我们从核心提案开始。对于每个trait <code>Foo</code>，我们向<code>dyn Foo</code>添加反映其方法的内在方法<sup id="fnref:3"><a href="#fn:3" rel="noopener noreferrer">3</a></sup>：</p>
<ul>
<li>对于<code>Foo</code>中每个<a href="https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility" rel="noopener noreferrer">动态兼容</a>的函数<code>f</code>，我们添加一个<code>&lt;dyn Foo&gt;::f</code>，它只是通过虚表调用<code>f</code>。</li>
<li>对于<code>Foo</code>中每个返回<code>impl Trait</code>值但除此之外是<a href="https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility" rel="noopener noreferrer">动态兼容</a>（例如，没有泛型参数<sup id="fnref:4"><a href="#fn:4" rel="noopener noreferrer">4</a></sup>，除了<code>self</code>参数外没有对<code>Self</code>的引用等）的函数<code>f</code>，我们添加一个<code>&lt;dyn Foo&gt;::f</code>方法，该方法被定义为返回一个<code>dyn Trait</code>。
<ul>
<li>这包括异步函数，它们是返回<code>impl Future</code>的函数的语法糖。</li>
</ul>
</li>
</ul>
<p>事实上，方法分发<em>已经</em>向<code>dyn Foo</code>添加了“伪”内在方法，因此在哪些方法被解析方面不会改变任何内容。区别在于，<code>dyn Foo</code>只有在trait中的所有方法都是动态兼容时才被允许，而根据此提案，一些非动态兼容的方法将以修改的签名被添加。</p>
<h2 id="change-1-dyn-compatibility">变化1：动态兼容性</h2>
<p>变化0只有在即使trait包含一些（例如异步函数）不动态兼容的方法，也可以创建<code>dyn Trait</code>的情况下才有意义。这重新审视了<a href="https://rust-lang.github.io/rfcs/0255-object-safety.html" rel="noopener noreferrer">RFC #255</a>，在该RFC中我们决定<code>dyn Trait</code>类型也应实现trait <code>Trait</code>。我当时是<a href="https://rust-lang.github.io/rfcs/0255-object-safety.html" rel="noopener noreferrer">RFC #255</a>的大力支持者，但后来我意识到我错了<sup id="fnref:5"><a href="#fn:5" rel="noopener noreferrer">5</a></sup>。让我们讨论一下。</p>
<p>当前允许<code>dyn Trait</code>实现<code>Trait</code>的两个规则如下：</p>
<ol>
<li>除非trait <code>Trait</code>是<em><a href="https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility" rel="noopener noreferrer">动态兼容</a></em>的（意味着它只有可以添加到虚表的方法），否则不允许<code>dyn Trait</code>。</li>
<li>要求<code>dyn Trait</code>中明确指定所有关联类型的值。因此<code>dyn Iterator&lt;Item = u32&gt;</code>是合法的，但单独的<code>dyn Iterator</code>不是。</li>
</ol>
<h3 id="dyn-compatibility-can-be-powerful">“动态兼容性”可能很强大</h3>
<p><code>dyn Trait</code>实现<code>Trait</code>这一事实有时相当强大。例如，它意味着我可以编写像这样的实现：</p>
<div><pre><code><span><span><span>struct</span><span> </span><span>RcWrapper</span><span>&lt;</span><span>T</span>: <span>?</span><span>Sized</span><span>&gt;</span><span> </span><span>{</span><span> </span><span>r</span>: <span>Rc</span><span>&lt;</span><span>RefCell</span><span>&lt;</span><span>T</span><span>&gt;&gt;</span><span> </span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>impl</span><span>&lt;</span><span>T</span><span>&gt;</span><span> </span><span>Iterator</span><span> </span><span>for</span><span> </span><span>RcWrapper</span><span>&lt;</span><span>T</span><span>&gt;</span><span>
</span></span></span><span><span><span>where</span><span>
</span></span></span><span><span><span>    </span><span>T</span>: <span>?</span><span>Sized</span><span> </span><span>+</span><span> </span><span>Iterator</span><span>,</span><span>
</span></span></span><span><span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>type</span> <span>Item</span><span> </span><span>=</span><span> </span><span>T</span>::<span>Item</span><span>;</span><span>
</span></span></span><span><span><span>    
</span></span></span><span><span><span>    </span><span>fn</span> <span>next</span><span>(</span><span>&amp;</span><span>mut</span><span> </span><span>self</span><span>)</span><span> </span>-&gt; <span>Option</span><span>&lt;</span><span>T</span>::<span>Item</span><span>&gt;</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>self</span><span>.</span><span>borrow_mut</span><span>().</span><span>next</span><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></pre></div><p>这个实现使得<code>RcWrapper&lt;I&gt;</code>对任何类型<code>I</code>实现<code>Iterator</code>，<em>包括</em>像<code>RcWrapper&lt;dyn Iterator&lt;Item = u32&gt;&gt;</code>这样的动态trait类型。很棒。</p>
<h3 id="dyn-compatibility-doesnt-truly-live-up-to-its-promise">“动态兼容性”并未真正兑现其承诺</h3>
<p>尽管强大，但<code>dyn Trait</code>实现<code>Trait</code>的想法并未完全兑现其承诺。你真正想要的是可以将任何<code>impl Trait</code>替换为<code>dyn Trait</code>并让一切正常工作。但这并不成立，因为<code>dyn Trait</code>是<code>?Sized</code>。所以实际上你并没有得到一个非常“流畅的体验”。此外，尽管编译器为你提供了<code>dyn Trait: Trait</code>实现，但它没有为<em>引用</em><code>dyn Trait</code>提供实现——所以例如，给定这个trait</p>
<div><pre><code><span><span><span>trait</span><span> </span><span>Compute</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>fn</span> <span>compute</span><span>(</span><span>&amp;</span><span>self</span><span>);</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>如果我有一个<code>Box&lt;dyn Compute&gt;</code>，我不能将其传递给接受<code>impl Compute</code>的函数</p>
<div><pre><code><span><span><span>fn</span> <span>do_compute</span><span>(</span><span>i</span>: <span>impl</span><span> </span><span>Compute</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>fn</span> <span>call_compute</span><span>(</span><span>b</span>: <span>Box</span><span>&lt;</span><span>dyn</span><span> </span><span>Compute</span><span>&gt;</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>do_compute</span><span>(</span><span>b</span><span>);</span><span> </span><span>// 错误
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>要使其工作，必须有人显式提供一个实现，比如</p>
<div><pre><code><span><span><span>impl</span><span>&lt;</span><span>I</span><span>&gt;</span><span> </span><span>Compute</span><span> </span><span>for</span><span> </span><span>Box</span><span>&lt;</span><span>I</span><span>&gt;</span><span>
</span></span></span><span><span><span>where</span><span>
</span></span></span><span><span><span>    </span><span>I</span>: <span>?</span><span>Sized</span><span>,</span><span>
</span></span></span><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></pre></div><p>而人们经常不这样做。</p>
<h3 id="dyn-compatibility-can-be-limiting">“动态兼容性”可能很限制</h3>
<p>然而，要求<code>dyn Trait</code>实现<code>Trait</code>可能很限制。想象一个像这样的trait</p>
<div><pre><code><span><span><span>trait</span><span> </span><span>ReportError</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>fn</span> <span>report</span><span>(</span><span>&amp;</span><span>self</span><span>,</span><span> </span><span>error</span>: <span>Error</span><span>);</span><span>
</span></span></span><span><span><span>    
</span></span></span><span><span><span>    </span><span>fn</span> <span>report_to</span><span>(</span><span>&amp;</span><span>self</span><span>,</span><span> </span><span>error</span>: <span>Error</span><span>,</span><span> </span><span>target</span>: <span>impl</span><span> </span><span>ErrorTarget</span><span>);</span><span>
</span></span></span><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></pre></div><p>这个trait有两个方法。<code>report</code>方法是动态兼容的，没问题。<code>report_to</code>方法有一个<code>impl Trait</code>参数，因此是泛型的，所以它不是动态兼容的<sup id="fnref:6"><a href="#fn:6" rel="noopener noreferrer">6</a></sup>（嗯，至少根据今天的规则是这样，但我会稍后讲到）。</p>
<p>（<code>report_to</code>不是动态兼容的原因：我们需要为<code>target</code>参数的类型制作不同的单态化副本。但虚表必须预先准备好，所以我们不知道使用哪个单态化版本。）</p>
<p>然而，仅仅因为<code>report_to</code>不是动态兼容的，并不意味着<code>dyn ReportError</code>将毫无用处。如果我只计划调用<code>report</code>，就像在这个函数中一样呢？</p>
<div><pre><code><span><span><span>fn</span> <span>report_all</span><span>(</span><span>
</span></span></span><span><span><span>    </span><span>errors</span>: <span>Vec</span><span>&lt;</span><span>Error</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>report</span>: <span>&amp;</span><span>dyn</span><span> </span><span>ReportError</span><span>,</span><span>
</span></span></span><span><span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>for</span><span> </span><span>e</span><span> </span><span>in</span><span> </span><span>errors</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>report</span><span>.</span><span>report</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></code></pre></div><p>Rust当前的规则排除了这样的函数，但实际上这种情况经常出现。事实上，它出现得如此频繁，以至于我们添加了一个语言特性来适应它（至少某种程度上）：你可以向你的特性添加一个<code>where Self: Sized</code>子句，以豁免其动态分发。这就是为什么<a href="https://doc.rust-lang.org/std/iter/trait.Iterator.html" rel="noopener noreferrer"><code>Iterator</code></a>即使有一堆泛型辅助方法（如<a href="https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.map" rel="noopener noreferrer"><code>map</code></a>和<a href="https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.flat_map" rel="noopener noreferrer"><code>flat_map</code></a>）也可以是动态兼容的。</p>
<h3 id="what-does-all-this-have-to-do-with-afidt">所有这些与异步函数中的动态trait（AFIDT）有什么关系？</h3>
<p>让我在这里暂停一下，因为我猜你们中的一些人想知道所有这些“动态兼容性”的东西与异步函数中的动态trait有什么关系。底线是要求<code>dyn Trait</code>类型实现<code>Trait</code>意味着我们不能在<code>dyn</code>分发上添加任何“特殊规则”，这与当你通过<code>dyn</code> trait调用异步函数时要求使用<code>.box</code>操作符不兼容。回想一下，使用我们的<code>Signal</code> trait，你可以在<code>impl Signal</code>上调用<code>signal</code>方法而无需任何装箱：</p>
<div><pre><code><span><span><span>async</span><span> </span><span>fn</span> <span>send_signal_1</span><span>(</span><span>impl_trait</span>: <span>&amp;</span><span>impl</span><span> </span><span>Signal</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>impl_trait</span><span>.</span><span>signal</span><span>().</span><span>await</span><span>;</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>但当我在<code>dyn Signal</code>上调用它时，我必须编写<code>.box</code>来告诉编译器如何处理返回的<code>dyn Future</code>：</p>
<div><pre><code><span><span><span>async</span><span> </span><span>fn</span> <span>send_signal_2</span><span>(</span><span>dyn_trait</span>: <span>&amp;</span><span>dyn</span><span> </span><span>Signal</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>dyn_trait</span><span>.</span><span>signal</span><span>().</span><span>box</span><span>.</span><span>await</span><span>;</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>确实，<code>Signal::signal</code>返回<code>impl Future</code>而<code>&lt;dyn Signal&gt;::signal</code>返回<code>dyn Future</code>这一事实已经证明了问题所在。所有<code>impl Future</code>类型都被认为是<code>Sized</code>的，而<code>dyn Future</code>不是，所以<code>&lt;dyn Signal&gt;::signal</code>的类型签名与trait中声明的类型签名不同。嗯。</p>
<h3 id="associated-type-values-are-needed-for-dyn-compatibility">关联类型值是动态兼容性所必需的</h3>
<p>今天我不能编写像<code>dyn Iterator</code>这样的类型而不指定关联类型<code>Item</code>的值。要理解为什么需要这个限制，请考虑这个泛型函数：</p>
<div><pre><code><span><span><span>fn</span> <span>drop_all</span><span>&lt;</span><span>I</span>: <span>?</span><span>Sized</span><span> </span><span>+</span><span> </span><span>Iterator</span><span>&gt;</span><span>(</span><span>iter</span>: <span>&amp;</span><span>mut</span><span> </span><span>I</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>while</span><span> </span><span>let</span><span> </span><span>Some</span><span>(</span><span>n</span><span>)</span><span> </span><span>=</span><span> </span><span>iter</span><span>.</span><span>next</span><span>()</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>std</span>::<span>mem</span>::<span>drop</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></code></pre></div><p>如果你用一个未指定<code>Item</code>的<code>&amp;mut dyn Iterator</code>调用<code>drop_all</code>，<code>n</code>的类型怎么可能知道？我们不知道它需要多少空间。但如果你用<code>&amp;mut dyn Iterator&lt;Item = u32&gt;</code>调用<code>drop_all</code>，就没有问题。我们不知道调用哪个<code>next</code>方法，但我们知道它返回一个<code>u32</code>。</p>
<h3 id="associated-type-values-are-limiting">关联类型值很限制</h3>
<p>然而，正如我们之前看到的，列出关联类型的要求可能很限制。如果我有一个<code>dyn Iterator</code>，并且我只调用<code>size_hint</code>，那么为什么我需要知道<code>Item</code>类型呢？</p>
<div><pre><code><span><span><span>fn</span> <span>size_hint</span><span>(</span><span>iter</span>: <span>&amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>Iterator</span><span>)</span><span> </span>-&gt; <span>bool</span> <span>{</span><span>
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>sh</span><span> </span><span>=</span><span> </span><span>iter</span><span>.</span><span>size_hint</span><span>();</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>但我今天不能编写这样的代码。相反，我必须使这个函数是泛型的，这基本上违背了使用<code>dyn Iterator</code>的整个目的：</p>
<div><pre><code><span><span><span>fn</span> <span>size_hint</span><span>&lt;</span><span>T</span><span>&gt;</span><span>(</span><span>iter</span>: <span>&amp;</span><span>mut</span><span> </span><span>dyn</span><span> </span><span>Iterator</span><span>&lt;</span><span>Item</span><span> </span><span>=</span><span> </span><span>T</span><span>&gt;</span><span>)</span><span> </span>-&gt; <span>bool</span> <span>{</span><span>
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>sh</span><span> </span><span>=</span><span> </span><span>iter</span><span>.</span><span>size_hint</span><span>();</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>如果我们放弃每个<code>dyn Iterator</code>类型都必须实现<code>Iterator</code>的要求，我们可以更有选择性，允许你调用不使用<code>Item</code>关联类型的方法，但不允许那些使用的方法。</p>
<h3 id="a-proposal-for-expanded-dyn-trait-usability">一个扩展<code>dyn Trait</code>可用性的提案</h3>
<p>这就引出了允许在trait未完全动态兼容的情况下使用<code>dyn Trait</code>的完整提案：</p>
<ul>
<li><code>dyn Trait</code>类型将被允许用于任何trait。<sup id="fnref:7"><a href="#fn:7" rel="noopener noreferrer">7</a></sup></li>
<li><code>dyn Trait</code>类型将不要求指定关联类型。</li>
<li>动态兼容的方法作为<code>dyn Trait</code>类型上的内在方法暴露。如果方法的签名引用了<code>dyn Trait</code>类型上未指定的关联类型，我们将不允许访问该方法。</li>
<li>如果trait是完全动态兼容的，那么指定其所有关联类型的<code>dyn Trait</code>将被视为实现<code>Trait</code>。<sup id="fnref:8"><a href="#fn:8" rel="noopener noreferrer">8</a></sup></li>
</ul>
<h2 id="the-box-keyword"><code>box</code>关键字</h2>
<blockquote>
<p>如果你愿意调用malloc，很多事情会变得更容易。</p>
<p>— Josh Triplett，最近。</p>
</blockquote>
<p>Rust从1.0起就保留了<code>box</code>关键字，但我们从未在稳定Rust中使用过它。最初的意图是<em>box</em>这个词将作为一个通用术语来指代任何“智能指针”类似的模式，所以<code>Rc</code>将是一个“引用计数的box”等等。然后<code>box</code>关键字将是一种通用的方式来分配任何类型的boxed值；与<code>Box::new</code>不同，它将进行“放置”，这样就不会分配中间值。随着时间的推移，我不再认为这是一个好主意。但我确实看到了拥有一个关键字来要求编译器自动创建<em>box</em>的很多价值。事实上，我看到很多地方都可以这样使用。</p>
<h3 id="boxed-expressions">boxed表达式</h3>
<p>第一个地方确实是<code>.box</code>操作符，可以用来将值放入box中。与<code>Box::new</code>不同，使用<code>.box</code>将允许编译器保证不会创建中间值，这种属性称为<em>放置</em>。考虑这个例子：</p>
<div><pre><code><span><span><span>fn</span> <span>main</span><span>()</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>x</span><span> </span><span>=</span><span> </span><span>Box</span>::<span>new</span><span>([</span><span>0_</span><span>u32</span><span>;</span><span> </span><span>1024</span><span>]);</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>Rust今天的语义要求（1）在栈上分配一个4KB缓冲区并将其清零；（2）在堆上分配一个box；然后（3）将内存从一处复制到另一处。这违反了我们的零成本抽象承诺：没有C程序员会编写这样的代码。但如果你写<code>[0_u32; 1024].box</code>，我们可以预先分配box并在原地初始化它。<sup id="fnref:9"><a href="#fn:9" rel="noopener noreferrer">9</a></sup></p>
<p>同样的原则适用于调用返回未固定大小类型的函数。这在今天是不允许的，但如果我们想让<code>async fn</code>返回<code>dyn Future</code>，我们需要某种方式来处理它。我们不能天真地支持它的原因是，在我们现有的ABI中，调用者负责分配足够的空间来存储返回值，并将该空间的地址传递给被调用者，被调用者然后写入其中。但对于<code>dyn Future</code>返回值，调用者无法知道分配多少空间。所以他们必须做其他事情，比如传入一个回调，该回调在给定正确空间量时执行分配。最常见的用例将是直接传入<code>malloc</code>。</p>
<p>对我来说，未固定大小返回值的最佳ABI尚不清楚，但我们不必现在解决它，ABI可以（并且应该）保持不稳定。但无论最终ABI是什么，当你在<code>.box</code>表达式的上下文中调用这样一个函数时，结果就是被调用者创建一个<code>Box</code>来存储结果。<sup id="fnref:10"><a href="#fn:10" rel="noopener noreferrer">10</a></sup></p>
<h3 id="boxed-async-functions-to-permit-recursion">boxed异步函数以允许递归</h3>
<p>如果你今天尝试编写一个调用自身的异步函数，你会得到一个错误：</p>
<div><pre><code><span><span><span>async</span><span> </span><span>fn</span> <span>fibonacci</span><span>(</span><span>a</span>: <span>u32</span><span>)</span><span> </span>-&gt; <span>u32</span> <span>{</span><span>
</span></span></span><span><span><span>    </span><span>match</span><span> </span><span>a</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>0</span><span> </span><span>=&gt;</span><span> </span><span>1</span><span>,</span><span>
</span></span></span><span><span><span>        </span><span>1</span><span> </span><span>=&gt;</span><span> </span><span>2</span><span>,</span><span>
</span></span></span><span><span><span>        </span><span>_</span><span> </span><span>=&gt;</span><span> </span><span>fibonacci</span><span>(</span><span>a</span><span>-</span><span>1</span><span>).</span><span>await</span><span> </span><span>+</span><span> </span><span>fibonacci</span><span>(</span><span>a</span><span>-</span><span>2</span><span>).</span><span>await</span><span>
</span></span></span><span><span><span>    </span><span>}</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>问题是我们无法静态确定分配多少栈空间。解决方案是重写为boxed返回值。这<a href="https://play.rust-lang.org/?version=stable&amp;mode=debug&amp;edition=2024&amp;gist=b36baf737a2811412e2970103fee25ee" rel="noopener noreferrer">可以编译</a>，因为编译器可以根据需要分配新的栈帧。</p>
<div><pre><code><span><span><span>fn</span> <span>fibonacci</span><span>(</span><span>a</span>: <span>u32</span><span>)</span><span> </span>-&gt; <span>Pin</span><span>&lt;</span><span>Box</span><span>&lt;</span><span>impl</span><span> </span><span>Future</span><span>&lt;</span><span>Output</span><span> </span><span>=</span><span> </span><span>u32</span><span>&gt;&gt;&gt;</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>Box</span>::<span>pin</span><span>(</span><span>async</span><span> </span><span>move</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>match</span><span> </span><span>a</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>            </span><span>0</span><span> </span><span>=&gt;</span><span> </span><span>1</span><span>,</span><span>
</span></span></span><span><span><span>            </span><span>1</span><span> </span><span>=&gt;</span><span> </span><span>2</span><span>,</span><span>
</span></span></span><span><span><span>            </span><span>_</span><span> </span><span>=&gt;</span><span> </span><span>fibonacci</span><span>(</span><span>a</span><span>-</span><span>1</span><span>).</span><span>await</span><span> </span><span>+</span><span> </span><span>fibonacci</span><span>(</span><span>a</span><span>-</span><span>2</span><span>).</span><span>await</span><span>
</span></span></span><span><span><span>        </span><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></pre></div><p>但如果我们能直接请求这个不是更好吗？</p>
<div><pre><code><span><span><span>box</span><span> </span><span>async</span><span> </span><span>fn</span> <span>fibonacci</span><span>(</span><span>a</span>: <span>u32</span><span>)</span><span> </span>-&gt; <span>u32</span> <span>{</span><span>
</span></span></span><span><span><span>    </span><span>match</span><span> </span><span>a</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>        </span><span>0</span><span> </span><span>=&gt;</span><span> </span><span>1</span><span>,</span><span>
</span></span></span><span><span><span>        </span><span>1</span><span> </span><span>=&gt;</span><span> </span><span>2</span><span>,</span><span>
</span></span></span><span><span><span>        </span><span>_</span><span> </span><span>=&gt;</span><span> </span><span>fibonacci</span><span>(</span><span>a</span><span>-</span><span>1</span><span>).</span><span>await</span><span> </span><span>+</span><span> </span><span>fibonacci</span><span>(</span><span>a</span><span>-</span><span>2</span><span>).</span><span>await</span><span>
</span></span></span><span><span><span>    </span><span>}</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><h3 id="boxed-structs-can-be-recursive">boxed结构体可以是递归的</h3>
<p>递归结构体会出现类似的问题：</p>
<div><pre><code><span><span><span>struct</span><span> </span><span>List</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>value</span>: <span>u32</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>next</span>: <span>Option</span><span>&lt;</span><span>List</span><span>&gt;</span><span>,</span><span> </span><span>// 错误
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>编译器告诉你</p>
<pre><code>error[E0072]: recursive type `List` has infinite size
 --&gt; src/lib.rs:1:1
  |
1 | struct List {
  | ^^^^^^^^^^^
2 |     value: u32,
3 |     next: Option&lt;List&gt;, // ERROR
  |                  ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&amp;`) to break the cycle
  |
3 |     next: Option&lt;Box&lt;List&gt;&gt;, // ERROR
  |                  ++++    +
</code></pre><p>正如它建议的，要解决这个问题，你可以引入一个<code>Box</code>：</p>
<div><pre><code><span><span><span>struct</span><span> </span><span>List</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>value</span>: <span>u32</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>next</span>: <span>Option</span><span>&lt;</span><span>Box</span><span>&lt;</span><span>List</span><span>&gt;&gt;</span><span>,</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>但这有点奇怪，因为现在列表的头是“内联”存储的，但未来的节点是堆分配的。我个人通常会采用更像这样的模式：</p>
<div><pre><code><span><span><span>struct</span><span> </span><span>List</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>data</span>: <span>Box</span><span>&lt;</span><span>ListData</span><span>&gt;</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>struct</span><span> </span><span>ListData</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>value</span>: <span>u32</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>next</span>: <span>Option</span><span>&lt;</span><span>List</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>然而，现在我不能用<code>List { value: 22, next: None }</code>语法创建值，也不能进行模式匹配。烦人。如果编译器在你声明结构体时建议添加<code>box</code>关键字，不是很好吗：</p>
<div><pre><code><span><span><span>box</span><span> </span><span>struct</span><span> </span><span>List</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>value</span>: <span>u32</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>next</span>: <span>Option</span><span>&lt;</span><span>List</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><p>并且让<code>List { value: 22, next: None }</code>自动为我分配box？理想的情况是box的存在现在是完全透明的，所以我可以完全透明地进行模式匹配等等：</p>
<div><pre><code><span><span><span>box</span><span> </span><span>struct</span><span> </span><span>List</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>value</span>: <span>u32</span><span>,</span><span>
</span></span></span><span><span><span>    </span><span>next</span>: <span>Option</span><span>&lt;</span><span>List</span><span>&gt;</span><span>,</span><span>
</span></span></span><span><span><span>}</span><span>
</span></span></span><span><span><span>
</span></span></span><span><span><span>fn</span> <span>foo</span><span>(</span><span>list</span>: <span>&amp;</span><span>List</span><span>)</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>let</span><span> </span><span>List</span><span> </span><span>{</span><span> </span><span>value</span><span>,</span><span> </span><span>next</span><span> </span><span>}</span><span> </span><span>=</span><span> </span><span>list</span><span>;</span><span> </span><span>// 等等
</span></span></span><span><span><span>}</span><span>
</span></span></span></code></pre></div><h3 id="boxed-enums-can-be-recursive-and-right-sized">boxed枚举可以是递归的<em>并且</em>大小合适</h3>
<p>枚举也不能引用自身。能够声明像这样的东西真的很好：</p>
<div><pre><code><span><span><span>box</span><span> </span><span>enum</span><span> </span><span>AstExpr</span><span> </span><span>{</span><span>
</span></span></span><span><span><span>    </span><span>Value</span><span>(</span><span>u32</span><span>),</span><span>
</span></span></span><span><span><span>    </span><span>If</span><span>(</span><span>AstExpr</span><span>,</span><span> </span><span></span></span></span></code></pre></div><p><em>由 mimo-v2.5 模型翻译，花费 40759 tokens</em></p>]]></content:encoded>
      <link>https://smallcultfollowing.com/babysteps/blog/2025/03/24/box-box-box/?utm_source=atom_feed</link>
      <guid isPermaLink="false">https://smallcultfollowing.com/babysteps/blog/2025/03/24/box-box-box/</guid>
      <pubDate>Mon, 24 Mar 2025 00:00:00 +0000</pubDate>
    </item>
  </channel>
</rss>
