<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Artemis Li</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <icon>https://ziling.moe/images/common/avatar-64.png</icon>
  <id>https://ziling.moe/</id>
  <link href="https://ziling.moe/" rel="alternate"/>
  <link href="https://ziling.moe/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, Artemis Li</rights>
  <subtitle>Artemis Li 的个人博客</subtitle>
  <title>穗织茶屋</title>
  <updated>2026-04-02T08:00:00.000Z</updated>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term=".NET Learning" scheme="https://ziling.moe/categories/NET-Learning/"/>
    <category term=".NET" scheme="https://ziling.moe/tags/NET/"/>
    <category term="面向对象" scheme="https://ziling.moe/tags/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1/"/>
    <category term="OOP" scheme="https://ziling.moe/tags/OOP/"/>
    <category term="跨语言" scheme="https://ziling.moe/tags/%E8%B7%A8%E8%AF%AD%E8%A8%80/"/>
    <content>
      <![CDATA[<p>很多时候，类与类的关系并不是<strong>is-a</strong>（是一个）组成的严格树形结构，而是<strong>has-a</strong>（包含）或<strong>can-do</strong>（具有能力）这种跨越层次结构的，横向的关系。</p><p>解决这个问题的热门方案并不止一个。</p><p><em>下文的 Trait 均为 Rust 的 Trait。</em></p><h2 id="范式"><a class="markdownIt-Anchor" href="#范式"></a> 范式</h2><h3 id="契约约束"><a class="markdownIt-Anchor" href="#契约约束"></a> 契约约束</h3><p>让我们从 C# 熟悉的接口说起。</p><p>在经典的 OOP 中，接口只是个<strong>行为契约</strong>，在实现上仅作为一组方法、属性等成员的声明，来约束实现该接口的类必须提供这些成员的实现。</p><p>纯粹的接口清晰地划分了定义与实现的职责，也很好地解决了多重继承冲突出现在接口的可能。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">interface</span> <span class="hljs-title">IControl</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">Paint</span>()</span>; <span class="hljs-comment">// 定义控件是可以被绘制的</span><br>&#125;<br><br><span class="hljs-keyword">interface</span> <span class="hljs-title">IWidget</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">Paint</span>()</span>; <span class="hljs-comment">// 定义小部件也是可以被绘制的</span><br>&#125;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title">Button</span> : <span class="hljs-title">IControl</span>, <span class="hljs-title">IWidget</span> <span class="hljs-comment">// 按钮既是控件也是小部件</span><br>&#123;<br>    <span class="hljs-keyword">void</span> IControl.Paint() <span class="hljs-comment">// 显式实现 IControl 的 Paint 方法</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;Painting control...&quot;</span>); <span class="hljs-comment">// 绘制控件的方式</span><br>    &#125;<br><br>    <span class="hljs-keyword">void</span> IWidget.Paint() <span class="hljs-comment">// 显式实现 IWidget 的 Paint 方法</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;Painting widget...&quot;</span>); <span class="hljs-comment">// 绘制小部件的方式</span><br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>尽管接口在实现子类型多态方面非常有效，但在代码复用上却无能为力，因为接口本身不包含任何实现细节。</p><h3 id="mixin"><a class="markdownIt-Anchor" href="#mixin"></a> Mixin</h3><p>Mixin 并不一定是一个语言的特性，而是一种设计模式，允许将一个类的功能混入另一个类中，从而实现代码复用。</p><p>C# 8.0 引入的<strong>默认接口实现</strong>被视为一种 Mixin 的实现方式，允许在接口中提供方法的默认实现，从而使得接口不仅仅是一个纯粹的契约，还可以包含一些行为的实现细节，详见<a href="/2026/dotnet-oop-interface">.NET 面向对象 - 接口</a>。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">interface</span> <span class="hljs-title">ILogger</span><br>&#123;<br>    <span class="hljs-comment">// 默认实现日志记录方法</span><br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">Log</span>(<span class="hljs-params"><span class="hljs-built_in">string</span> message</span>)</span> =&gt; Console.WriteLine(<span class="hljs-string">$&quot;Log: <span class="hljs-subst">&#123;message&#125;</span>&quot;</span>);<br>&#125;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title">FileLogger</span> : <span class="hljs-title">ILogger</span><br>&#123;<br>    <span class="hljs-comment">// 无需额外实现 Log 方法</span><br>&#125;<br></code></pre></td></tr></table></figure><p>从这个例子来看，Mixin 似乎并不是什么高级的特性，无非有点像是类的继承，也提供了可以修改的默认实现。</p><p>那 Mixin 解决了继承的什么问题？</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Appliance</span>: <span class="hljs-comment"># 基类：是什么</span><br>    <span class="hljs-keyword">def</span> <span class="hljs-title function_">__init__</span>(<span class="hljs-params">self, brand</span>):<br>        <span class="hljs-variable language_">self</span>.brand = brand<br><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">WifiMixin</span>: <span class="hljs-comment"># Mixin：能做什么</span><br>    <span class="hljs-keyword">def</span> <span class="hljs-title function_">connect</span>(<span class="hljs-params">self</span>):<br>        <span class="hljs-built_in">print</span>(<span class="hljs-string">f&quot;正在连接网络...&quot;</span>)<br><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">MusicMixin</span>: <span class="hljs-comment"># Mixin：能做什么</span><br>    <span class="hljs-keyword">def</span> <span class="hljs-title function_">play_music</span>(<span class="hljs-params">self</span>):<br>        <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;正在播放：🎵 Classic Jazz...&quot;</span>)<br><br><span class="hljs-comment"># 组合</span><br><br><span class="hljs-comment"># 智能冰箱：既是电器，又能联网</span><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">SmartFridge</span>(Appliance, WifiMixin):<br>    <span class="hljs-keyword">pass</span><br><br><span class="hljs-comment"># 智能音箱：既是电器，又能联网，还能放音乐</span><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">SmartSpeaker</span>(Appliance, WifiMixin, MusicMixin):<br>    <span class="hljs-keyword">pass</span><br><br><span class="hljs-comment"># 使用</span><br>speaker = SmartSpeaker(<span class="hljs-string">&quot;Sonos&quot;</span>)<br>speaker.connect()      <span class="hljs-comment"># 来自 WifiMixin</span><br>speaker.play_music()   <span class="hljs-comment"># 来自 MusicMixin</span><br></code></pre></td></tr></table></figure><p>从代码上就会发现，这5个类都没有出现什么继承关系，更像是让类<strong>包含</strong>了什么功能 <em>(can-do)</em>。</p><p>也就是说 Mixin：</p><ul><li>让一个类可以包含多个功能，而不是继承自多个父类。</li><li>能实现代码复用。</li><li>与继承后带着父类的一切特性不同，Mixin 可以让类只包含它需要的功能。</li></ul><h3 id="trait"><a class="markdownIt-Anchor" href="#trait"></a> Trait</h3><p>在接口与 Mixin 的特性之上，Trait 既像接口一样定义方法签名，又像 Mixin 一样提供行为实现。</p><p>以 Rust 为例，假如存在简单的圆和矩形结构体，需要计算它们的面积。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">struct</span> <span class="hljs-title class_">Circle</span> &#123; radius: <span class="hljs-type">f64</span> &#125;<br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">Rectangle</span> &#123; width: <span class="hljs-type">f64</span>, height: <span class="hljs-type">f64</span> &#125;<br><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">get_circle_area</span>(c: &amp;Circle) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">f64</span> &#123;<br>    <span class="hljs-number">3.14</span> * c.radius * c.radius<br>&#125;<br><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">get_rect_area</span>(r: &amp;Rectangle) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">f64</span> &#123;<br>    r.width * r.height<br>&#125;<br></code></pre></td></tr></table></figure><p>借助 Trait，我们可以定义一个 <code>Shape</code> trait 来抽象出所有形状都应该具有的 <code>area</code> 方法，并为每个具体的形状提供实现。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">trait</span> <span class="hljs-title class_">HasArea</span> &#123;<br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">area</span>(&amp;<span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">f64</span>;<br>&#125;<br><br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">Circle</span> &#123; radius: <span class="hljs-type">f64</span> &#125;<br><span class="hljs-keyword">impl</span> <span class="hljs-title class_">HasArea</span> <span class="hljs-keyword">for</span> <span class="hljs-title class_">Circle</span> &#123;<br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">area</span>(&amp;<span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">f64</span> &#123;<br>        std::<span class="hljs-type">f64</span>::consts::PI * <span class="hljs-keyword">self</span>.radius * <span class="hljs-keyword">self</span>.radius<br>    &#125;<br>&#125;<br><br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">Rectangle</span> &#123; width: <span class="hljs-type">f64</span>, height: <span class="hljs-type">f64</span> &#125;<br><span class="hljs-keyword">impl</span> <span class="hljs-title class_">HasArea</span> <span class="hljs-keyword">for</span> <span class="hljs-title class_">Rectangle</span> &#123;<br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">area</span>(&amp;<span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">f64</span> &#123;<br>        <span class="hljs-keyword">self</span>.width * <span class="hljs-keyword">self</span>.height<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>如果需要计算一个形状的面积，我们就可以使用 Trait 来实现多态：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">fn</span> <span class="hljs-title function_">print_area</span>&lt;T: HasArea&gt;(shape: &amp;T) &#123;<br>    <span class="hljs-built_in">println!</span>(<span class="hljs-string">&quot;面积: &#123;&#125;&quot;</span>, shape.<span class="hljs-title function_ invoke__">area</span>());<br>&#125;<br></code></pre></td></tr></table></figure><p>在这个例子中，Trait 扮演的用途很像带有默认实现的接口，但它的强度不止于此。</p><h4 id="后验"><a class="markdownIt-Anchor" href="#后验"></a> 后验</h4><p>类似于 C# 的扩展方法，Trait 还可以在不修改原有类型定义的情况下，为现有类型添加新的方法。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-comment">// 假设 Circle 和 Rectangle 在第三方库中定义</span><br><span class="hljs-keyword">trait</span> <span class="hljs-title class_">HasPerimeter</span> &#123;<br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">perimeter</span>(&amp;<span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">f64</span>;<br>&#125;<br><span class="hljs-keyword">impl</span> <span class="hljs-title class_">HasPerimeter</span> <span class="hljs-keyword">for</span> <span class="hljs-title class_">Circle</span> &#123;<br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">perimeter</span>(&amp;<span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">f64</span> &#123;<br>        <span class="hljs-number">2.0</span> * std::<span class="hljs-type">f64</span>::consts::PI * <span class="hljs-keyword">self</span>.radius<br>    &#125;<br>&#125;<br><span class="hljs-keyword">impl</span> <span class="hljs-title class_">HasPerimeter</span> <span class="hljs-keyword">for</span> <span class="hljs-title class_">Rectangle</span> &#123;<br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">perimeter</span>(&amp;<span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">f64</span> &#123;<br>        <span class="hljs-number">2.0</span> * (<span class="hljs-keyword">self</span>.width + <span class="hljs-keyword">self</span>.height)<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>通过这种方式，我们可以在不修改 <code>Circle</code> 和 <code>Rectangle</code> 结构体的定义的情况下，为它们添加计算周长的方法。</p><p>至此，C# 的默认接口实现配合扩展方法已经能够实现 Trait 的大部分功能了，但是仍然缺点东西。</p><h4 id="非侵入"><a class="markdownIt-Anchor" href="#非侵入"></a> 非侵入</h4><p>扩展方法解决了后验问题，但它的功能没那么强大：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">trait</span> <span class="hljs-title class_">Speak</span> &#123;<br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">talk</span>(&amp;<span class="hljs-keyword">self</span>);<br>&#125;<br><span class="hljs-keyword">impl</span> <span class="hljs-title class_">Speak</span> <span class="hljs-keyword">for</span> <span class="hljs-title class_">i32</span> &#123;<br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">talk</span>(&amp;<span class="hljs-keyword">self</span>) &#123;<br>        <span class="hljs-built_in">println!</span>(<span class="hljs-string">&quot;我是数字: &#123;&#125;&quot;</span>, <span class="hljs-keyword">self</span>);<br>    &#125;<br>&#125;<br><br><span class="hljs-keyword">fn</span> <span class="hljs-title function_">do_something</span>&lt;T: Speak&gt;(item: T) &#123;<br>    item.<span class="hljs-title function_ invoke__">talk</span>();<br>&#125;<br><br><span class="hljs-title function_ invoke__">do_something</span>(<span class="hljs-number">42</span>);<br></code></pre></td></tr></table></figure><p>以上代码在 C# 该怎么写呢？</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">interface</span> <span class="hljs-title">ISpeak</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">Talk</span>()</span>;<br>&#125;<br><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">IntSpeak</span> : <span class="hljs-title">ISpeak</span><br>&#123;<br>    <span class="hljs-keyword">private</span> <span class="hljs-built_in">int</span> _value;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">IntSpeak</span>(<span class="hljs-params"><span class="hljs-built_in">int</span> <span class="hljs-keyword">value</span></span>)</span><br>    &#123;<br>        _value = <span class="hljs-keyword">value</span>;<br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Talk</span>()</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">$&quot;我是数字: <span class="hljs-subst">&#123;_value&#125;</span>&quot;</span>);<br>    &#125;<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">DoSomething</span>&lt;<span class="hljs-title">T</span>&gt;(<span class="hljs-params">T item</span>) <span class="hljs-keyword">where</span> T : ISpeak</span><br>&#123;<br>    item.Talk();<br>&#125;<br><br>DoSomething(<span class="hljs-keyword">new</span> IntSpeak(<span class="hljs-number">42</span>));<br></code></pre></td></tr></table></figure><p>扩展方法仅仅是静态方法的语法糖，无法真正地将方法附加到类型上，因此在使用时需要额外的包装类来实现接口，这就达不到 Trait 的非侵入式特性了。</p><h4 id="泛实现"><a class="markdownIt-Anchor" href="#泛实现"></a> 泛实现</h4><p>Trait 还支持泛实现 <em>(Blanket Implementation)</em>，允许为满足特定条件的类型自动实现 Trait。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">impl</span>&lt;T&gt; IsEmpty <span class="hljs-keyword">for</span> <span class="hljs-title class_">T</span> <span class="hljs-keyword">where</span> T: HasLength &#123;<br>    <span class="hljs-keyword">fn</span> <span class="hljs-title function_">is_empty</span>(&amp;<span class="hljs-keyword">self</span>) <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">bool</span> &#123;<br>        <span class="hljs-keyword">self</span>.<span class="hljs-title function_ invoke__">len</span>() == <span class="hljs-number">0</span><br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>然而，接口并不支持这种泛实现的特性。</p><h4 id="性能"><a class="markdownIt-Anchor" href="#性能"></a> 性能</h4><h5 id="c-与接口与去虚化"><a class="markdownIt-Anchor" href="#c-与接口与去虚化"></a> C# 与接口与去虚化</h5><p>C# 的接口调用在 IL 层通常是<code>callvirt</code>指令。</p><p>在默认情况下，接口方法调用会涉及到虚函数表，先找对象的类型信息，再找函数地址，最后跳过去。</p><p>不过有些时候，如果编译器在运行时发现某个接口调用实际上是单态的（即只被一个具体类型实现），它就可以进行<strong>去虚化</strong>优化，直接将接口调用转换为普通的函数调用，从而避免虚函数表的开销。</p><p>同样的，借助 JIT 的分层编译 <em>(Tiered Compilation)</em>，编译器可以先快速捏一个较低性能的版本，随着运行时发现一段代码被频繁调用，再重新编译并进行更复杂的去虚化优化。</p><h5 id="rust-与单态化"><a class="markdownIt-Anchor" href="#rust-与单态化"></a> Rust 与单态化</h5><p>对于以下：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs rust"><span class="hljs-keyword">fn</span> <span class="hljs-title function_">print_area</span>&lt;T: HasArea&gt;(shape: &amp;T) &#123;<br>    <span class="hljs-built_in">println!</span>(<span class="hljs-string">&quot;面积: &#123;&#125;&quot;</span>, shape.<span class="hljs-title function_ invoke__">area</span>());<br>&#125;<br></code></pre></td></tr></table></figure><p>编译器会为每个调用该函数的具体类型生成独立的副本，从而让这类函数调用都是 Direct Call，没有虚函数表的开销。</p><p>但单态化本身是个听起来就比较费时费力的过程，编译器要干的活变多，编译出的二进制文件也会变大。</p><p>至少大部分工作是在编译阶段完成的，运行时这方面的性能是非常不错的。</p><h2 id="菱形继承问题"><a class="markdownIt-Anchor" href="#菱形继承问题"></a> 菱形继承问题</h2><p>依旧是经典批判对象。</p><p>在经典的 OOP 中，如果一个类同时继承自两个父类，而这两个父类又有一个共同的祖先，就会出现所谓的<strong>菱形继承问题</strong>。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs plaintext">  A<br> / \<br>B   C<br> \ /<br>  D<br></code></pre></td></tr></table></figure><h3 id="mixin-方案"><a class="markdownIt-Anchor" href="#mixin-方案"></a> Mixin 方案</h3><p>在 Scala 中，Mixin 将继承结构拍平，借助 Mixin 的线性化算法来解决菱形继承问题。</p><figure class="highlight scala"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs scala"><span class="hljs-class"><span class="hljs-keyword">trait</span> <span class="hljs-title">BaseLogger</span> </span>&#123;<br>  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">log</span></span>(msg: <span class="hljs-type">String</span>): <span class="hljs-type">Unit</span> = println(<span class="hljs-string">s&quot;[Base] <span class="hljs-subst">$msg</span>&quot;</span>)<br>&#125;<br><br><span class="hljs-class"><span class="hljs-keyword">trait</span> <span class="hljs-title">ConsoleLogger</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">BaseLogger</span> </span>&#123;<br>  <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">log</span></span>(msg: <span class="hljs-type">String</span>): <span class="hljs-type">Unit</span> = &#123;<br>    print(<span class="hljs-string">&quot;[Console]&quot;</span>)<br>    <span class="hljs-keyword">super</span>.log(msg) <span class="hljs-comment">// 这 super 指谁？</span><br>  &#125;<br>&#125;<br><br><span class="hljs-class"><span class="hljs-keyword">trait</span> <span class="hljs-title">FileLogger</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">BaseLogger</span> </span>&#123;<br>  <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">log</span></span>(msg: <span class="hljs-type">String</span>): <span class="hljs-type">Unit</span> = &#123;<br>    print(<span class="hljs-string">&quot;[File]&quot;</span>)<br>    <span class="hljs-keyword">super</span>.log(msg) <span class="hljs-comment">// 这 super 指谁？</span><br>  &#125;<br>&#125;<br><br><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SavingApp</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">ConsoleLogger</span> <span class="hljs-keyword">with</span> <span class="hljs-title">FileLogger</span></span><br><br><span class="hljs-keyword">val</span> app = <span class="hljs-keyword">new</span> <span class="hljs-type">SavingApp</span><br>app.log(<span class="hljs-string">&quot;Saved&quot;</span>)<br></code></pre></td></tr></table></figure><p>在这个例子中，<code>SavingApp</code> 同时混入了 <code>ConsoleLogger</code> 和 <code>FileLogger</code>，它们都继承自 <code>BaseLogger</code>。</p><p>Scala 会尝试从右向左，深度优先，去重地处理：</p><ol><li>基于当前类 <code>SavingApp</code></li><li>然后到最右侧的 <code>FileLogger</code>，及其继承的 <code>BaseLogger</code></li><li>再到 <code>ConsoleLogger</code>，及其继承的 <code>BaseLogger</code></li><li>按照<code>SavingApp -&gt; FileLogger -&gt; ConsoleLogger -&gt; BaseLogger</code>的顺序线性化</li></ol><p>于是调用<code>app.log(&quot;Saved&quot;)</code>时，Scala 会：</p><ol><li>进入<code>FileLogger</code>的<code>log</code>方法，输出<code>[File]</code></li><li>在<code>FileLogger</code>中调用<code>super.log(msg)</code></li><li>根据线性化顺序，进入<code>ConsoleLogger</code>的<code>log</code>方法，输出<code>[Console]</code></li><li>在<code>ConsoleLogger</code>中调用<code>super.log(msg)</code></li><li>最后进入<code>BaseLogger</code>的<code>log</code>方法，输出<code>[Base] Saved</code></li><li>最终输出结果为：<code>[File] [Console] [Base] Saved</code></li></ol><p><code>super</code>这样跑来跑去的，确实解决二义性了，但认知负担也上来了。</p><h3 id="接口与-trait-方案"><a class="markdownIt-Anchor" href="#接口与-trait-方案"></a> 接口与 Trait 方案</h3><p>接口的约束在签名上，在定义时阻止了二义性即可。</p><p>一个容易被判定为菱形继承问题的 C# 代码：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">interface</span> <span class="hljs-title">IFly</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">Fly</span>()</span>;<br>&#125;<br><span class="hljs-keyword">interface</span> <span class="hljs-title">IBird</span> : <span class="hljs-title">IFly</span> &#123; &#125;<br><span class="hljs-keyword">interface</span> <span class="hljs-title">IPlane</span> : <span class="hljs-title">IFly</span> &#123; &#125;<br><span class="hljs-keyword">class</span> <span class="hljs-title">Drone</span> : <span class="hljs-title">IBird</span>, <span class="hljs-title">IPlane</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Fly</span>()</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;Drone is flying&quot;</span>);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>编译器对这段代码没意见，因为它本来就不存在二义性，所以可以说这个类的接口实现是菱形的关系，但它并没有引入问题。</p><p>但是如果我们在接口中提供了默认实现：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">interface</span> <span class="hljs-title">IFly</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">Fly</span>()</span> =&gt; Console.WriteLine(<span class="hljs-string">&quot;Flying...&quot;</span>);<br>&#125;<br><span class="hljs-keyword">interface</span> <span class="hljs-title">IBird</span> : <span class="hljs-title">IFly</span> &#123; &#125;<br><span class="hljs-keyword">interface</span> <span class="hljs-title">IPlane</span> : <span class="hljs-title">IFly</span> &#123; &#125;<br><span class="hljs-keyword">class</span> <span class="hljs-title">Drone</span> : <span class="hljs-title">IBird</span>, <span class="hljs-title">IPlane</span><br>&#123;<br>    <span class="hljs-comment">// 这里就会出现二义性了</span><br>&#125;<br></code></pre></td></tr></table></figure><p>或者</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">interface</span> <span class="hljs-title">IFly</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">Fly</span>()</span>;<br>&#125;<br><span class="hljs-keyword">interface</span> <span class="hljs-title">IBird</span> : <span class="hljs-title">IFly</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">Fly</span>()</span> =&gt; Console.WriteLine(<span class="hljs-string">&quot;Bird is flying...&quot;</span>);<br>&#125;<br><span class="hljs-keyword">interface</span> <span class="hljs-title">IPlane</span> : <span class="hljs-title">IFly</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">Fly</span>()</span> =&gt; Console.WriteLine(<span class="hljs-string">&quot;Plane is flying...&quot;</span>);<br>&#125;<br><span class="hljs-keyword">class</span> <span class="hljs-title">Drone</span> : <span class="hljs-title">IBird</span>, <span class="hljs-title">IPlane</span><br>&#123;<br>    <span class="hljs-comment">// 这里也会出现二义性</span><br>&#125;<br></code></pre></td></tr></table></figure><p>这俩情况都可以在编译时被检测出来，所以 C# 的接口在默认实现的情况下也能很好地避免菱形继承问题。</p><p>Trait 和接口都是显式地要求开发者在定义时就解决二义性问题的，而不是采取类似 Scala Mixin 那样隐式地通过线性化算法来解决。</p><h2 id="c-的演变"><a class="markdownIt-Anchor" href="#c-的演变"></a> C# 的演变</h2><p>如果将 <code>Interface / Mixin -&gt; Trait</code> 看作是一个演变的过程，C# 除了接口、默认接口实现和扩展方法之外，依然还在前进。</p><h3 id="静态抽象成员"><a class="markdownIt-Anchor" href="#静态抽象成员"></a> 静态抽象成员</h3><p>与默认接口实现让 C# 在实例上接近 Trait 不同，静态抽象成员则让 C# 在类型上接近 Trait。</p><p>在接口中定义一个规则，要求实现该接口的类型必须提供一个静态方法。</p><p>比如写一个解析器接口：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IParsable</span>&lt;<span class="hljs-title">TSelf</span>&gt; <span class="hljs-keyword">where</span> <span class="hljs-title">TSelf</span> : <span class="hljs-title">IParsable</span>&lt;<span class="hljs-title">TSelf</span>&gt;<br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">static</span> <span class="hljs-keyword">abstract</span> TSelf <span class="hljs-title">Parse</span>(<span class="hljs-params"><span class="hljs-built_in">string</span> input</span>)</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>实现这个接口的类型必须提供一个<strong>静态</strong>的 <code>Parse</code> 方法：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">struct</span> Point : IParsable&lt;Point&gt;<br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> X &#123; <span class="hljs-keyword">get</span>; &#125;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> Y &#123; <span class="hljs-keyword">get</span>; &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Point</span>(<span class="hljs-params"><span class="hljs-built_in">int</span> x, <span class="hljs-built_in">int</span> y</span>)</span><br>    &#123;<br>        X = x;<br>        Y = y;<br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Point <span class="hljs-title">Parse</span>(<span class="hljs-params"><span class="hljs-built_in">string</span> input</span>)</span><br>    &#123;<br>        <span class="hljs-keyword">var</span> parts = input.Split(<span class="hljs-string">&#x27;,&#x27;</span>);<br>        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Point(<span class="hljs-built_in">int</span>.Parse(parts[<span class="hljs-number">0</span>]), <span class="hljs-built_in">int</span>.Parse(parts[<span class="hljs-number">1</span>]));<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>然后强大之处在于在写某个方法时，处理任何支持解析的类型都不需要关心具体类型：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> T <span class="hljs-title">CreateFromInput</span>&lt;<span class="hljs-title">T</span>&gt;(<span class="hljs-params"><span class="hljs-built_in">string</span> input</span>) <span class="hljs-keyword">where</span> T : IParsable&lt;T&gt;</span><br>&#123;<br>    <span class="hljs-keyword">return</span> T.Parse(input); <span class="hljs-comment">// 不需要实例化 T</span><br>&#125;<br></code></pre></td></tr></table></figure><hr /><p>同样，泛型数学也可以借助静态抽象成员来实现，比如货币计算：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IAddable</span>&lt;<span class="hljs-title">TSelf</span>&gt; <span class="hljs-keyword">where</span> <span class="hljs-title">TSelf</span> : <span class="hljs-title">IAddable</span>&lt;<span class="hljs-title">TSelf</span>&gt;<br>&#123;<br>    <span class="hljs-keyword">static</span> <span class="hljs-keyword">abstract</span> TSelf <span class="hljs-keyword">operator</span> +(TSelf left, TSelf right);<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">record</span> <span class="hljs-title">Money</span>(<span class="hljs-params"><span class="hljs-built_in">decimal</span> Amount</span>) : IAddable&lt;Money&gt;</span><br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Money <span class="hljs-keyword">operator</span> +(Money left, Money right)<br>        =&gt; <span class="hljs-keyword">new</span> Money(left.Amount + right.Amount);<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">public</span> T <span class="hljs-title">Add</span>&lt;<span class="hljs-title">T</span>&gt;(<span class="hljs-params">T a, T b</span>) <span class="hljs-keyword">where</span> T : IAddable&lt;T&gt;</span><br>&#123;<br>    <span class="hljs-keyword">return</span> a + b; <span class="hljs-comment">// 直接使用 + 运算符</span><br>&#125;<br></code></pre></td></tr></table></figure><h3 id="有状态的-mixin"><a class="markdownIt-Anchor" href="#有状态的-mixin"></a> 有状态的 Mixin</h3><p>在 .NET 动态语言中（如 IronPython），为了满足它们在不改变对象原始内存布局的前提下向对象附加属性的需求，如果使用<code>Dictionary&lt;string, object&gt;</code>在外部维护 Mixin 的状态，由于 Key 会持有对象的强引用，GC 将无法回收这些对象。</p><p>于是，C# 提供了一个泛型集合：<code>ConditionalWeakTable&lt;TKey, TValue&gt;</code>。</p><p>观察<code>ConditionalWeakTable</code>中的<code>CreateEntryNoResize</code>：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">internal</span> <span class="hljs-keyword">void</span> <span class="hljs-title">CreateEntryNoResize</span>(<span class="hljs-params">TKey key, TValue <span class="hljs-keyword">value</span></span>)</span><br>&#123;<br>    <span class="hljs-comment">// 省略</span><br>    <span class="hljs-built_in">int</span> hashCode = RuntimeHelpers.GetHashCode(key) &amp; <span class="hljs-built_in">int</span>.MaxValue;<br>    <span class="hljs-built_in">int</span> newEntry = _firstFreeEntry++;<br><br>    _entries[newEntry].HashCode = hashCode;<br>    _entries[newEntry].depHnd = <span class="hljs-keyword">new</span> DependentHandle(key, <span class="hljs-keyword">value</span>);<br>    <span class="hljs-comment">// 省略</span><br>&#125;<br></code></pre></td></tr></table></figure><p>会发现这个集合维护的 Key 是个 <code>DependentHandle</code>。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">DependentHandle</span>(<span class="hljs-params"><span class="hljs-built_in">object</span>? target, <span class="hljs-built_in">object</span>? dependent</span>)</span><br>&#123;<br>    IntPtr handle = InternalAlloc(target, dependent);<br>    <span class="hljs-keyword">if</span> (handle == <span class="hljs-number">0</span>)<br>        handle = InternalAllocWithGCTransition(target, dependent);<br>    _handle = handle;<br>&#125;<br></code></pre></td></tr></table></figure><p>它是一个特殊的句柄，允许 GC 在 Key 不再被其他对象引用时回收它，同时也会自动清理与之关联的 Value。</p><hr /><p>体现为有状态的 Mixin，可以借助这个类来实现：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Person</span><br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> Name &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>&#125;<br><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">TaggedMixin</span><br>&#123;<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">readonly</span> ConditionalWeakTable&lt;<span class="hljs-built_in">object</span>, TagState&gt; _table = <span class="hljs-keyword">new</span>();<br><br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">class</span> <span class="hljs-title">TagState</span><br>    &#123;<br>        <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> Tag &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125; = <span class="hljs-string">&quot;None&quot;</span>;<br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-built_in">string</span> <span class="hljs-title">GetTag</span>(<span class="hljs-params"><span class="hljs-keyword">this</span> <span class="hljs-built_in">object</span> obj</span>)</span><br>    &#123;<br>        <span class="hljs-keyword">return</span> _table.GetOrCreateValue(obj).Tag;<br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">SetTag</span>(<span class="hljs-params"><span class="hljs-keyword">this</span> <span class="hljs-built_in">object</span> obj, <span class="hljs-built_in">string</span> <span class="hljs-keyword">value</span></span>)</span><br>    &#123;<br>        _table.GetOrCreateValue(obj).Tag = <span class="hljs-keyword">value</span>;<br>    &#125;<br>&#125;<br><br><span class="hljs-keyword">var</span> p1 = <span class="hljs-keyword">new</span> Person &#123; Name = <span class="hljs-string">&quot;Alice&quot;</span> &#125;;<br><span class="hljs-keyword">var</span> p2 = <span class="hljs-keyword">new</span> Person &#123; Name = <span class="hljs-string">&quot;Bob&quot;</span> &#125;;<br><br><span class="hljs-comment">// 像使用实例属性一样设置状态</span><br>p1.SetTag(<span class="hljs-string">&quot;Admin&quot;</span>);<br>p2.SetTag(<span class="hljs-string">&quot;User&quot;</span>);<br><br>Console.WriteLine(<span class="hljs-string">$&quot;<span class="hljs-subst">&#123;p1.Name&#125;</span> 的标签是: <span class="hljs-subst">&#123;p1.GetTag()&#125;</span>&quot;</span>);<br>Console.WriteLine(<span class="hljs-string">$&quot;<span class="hljs-subst">&#123;p2.Name&#125;</span> 的标签是: <span class="hljs-subst">&#123;p2.GetTag()&#125;</span>&quot;</span>);<br></code></pre></td></tr></table></figure><h3 id="源生成器"><a class="markdownIt-Anchor" href="#源生成器"></a> 源生成器</h3><p>随着 .NET 的 Roslyn 增量源生成器 <em>(Incremental Source Generators)</em> 的引入，开发者可以在编译时生成代码，从而在不修改原有类型定义的情况下为它们添加新的功能。</p><p>源生成器在编译前期分析 AST 等信息，借助<strong>特性</strong> <em>(Attribute)</em> 来定位到指定的 Mixin 模板类，并生成一个与目标类同名的新<code>partial</code>类来实现 Mixin 的功能。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs csharp">[<span class="hljs-meta">GenerateMixin(typeof(LoggingMixin))</span>]<br><span class="hljs-keyword">public</span> <span class="hljs-keyword">partial</span> <span class="hljs-keyword">class</span> <span class="hljs-title">MyClass</span> &#123; &#125;<br><br><span class="hljs-comment">// 源生成器生成后</span><br><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">partial</span> <span class="hljs-keyword">class</span> <span class="hljs-title">MyClass</span> : <span class="hljs-title">ILoggingMixin</span><br>&#123;<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> LoggingMixin _mixin = <span class="hljs-keyword">new</span> LoggingMixin();<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Log</span>(<span class="hljs-params"><span class="hljs-built_in">string</span> message</span>)</span><br>    &#123;<br>        _mixin.Log(message);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>在现代 .NET 中，由于这种实现（如<code>CommunityToolkit.Mvvm</code>的属性生成）可以达成零成本抽象，同时满足 AOT 编译要求，它变得更加主流。</p><h3 id="extension-everything"><a class="markdownIt-Anchor" href="#extension-everything"></a> Extension Everything</h3><p>2016年，<a href="https://github.com/dotnet/roslyn/issues/11159">dotnet/roslyn - Extension Everything</a> 提案的核心目标是让开发者不再局限于扩展方法，还能扩展属性、静态成员、事件、操作符等等。</p><p>提案早期的形式如下：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">public</span> extension <span class="hljs-keyword">class</span> <span class="hljs-title">ListListExtensions</span>&lt;<span class="hljs-title">T</span>&gt;<br>  : <span class="hljs-title">List</span>&lt;<span class="hljs-title">List</span>&lt;<span class="hljs-title">T</span>&gt;&gt; <br>  <span class="hljs-comment">// , Interface</span><br>&#123;<br>   <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-built_in">int</span> _flattenCount = <span class="hljs-number">0</span>;<br><br>   <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-built_in">int</span> <span class="hljs-title">GetFlattenCount</span>()</span><br>   &#123;<br>     <span class="hljs-keyword">return</span> _flattenCount;<br>   &#125;<br><br>   <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-built_in">int</span> FlattenCount<br>   &#123;<br>       <span class="hljs-keyword">get</span><br>       &#123;<br>         <span class="hljs-keyword">return</span> _flattenCount;<br>       &#125;<br>   &#125;<br><br>   <span class="hljs-function"><span class="hljs-keyword">public</span> List&lt;T&gt; <span class="hljs-title">Flatten</span>()</span><br>   &#123;<br>     _flattenCount++;<br>     ...<br>   &#125;<br><br>   <span class="hljs-keyword">public</span> List&lt;List&lt;T&gt;&gt; <span class="hljs-keyword">this</span>[<span class="hljs-built_in">int</span>[] indices] =&gt; ...;<br><br>   <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">implicit</span> <span class="hljs-keyword">operator</span> <span class="hljs-title">List</span>&lt;<span class="hljs-title">T</span>&gt;(<span class="hljs-params">List&lt;List&lt;T&gt;&gt; self</span>)</span><br>   &#123;<br>     <span class="hljs-keyword">return</span> self.Flatten();<br>   &#125;<br><br>   <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">implicit</span> List&lt;List&lt;T&gt;&gt; <span class="hljs-keyword">operator</span> +(List&lt;List&lt;T&gt;&gt; left, List&lt;List&lt;T&gt;&gt; right) =&gt; left.Concat(right);<br>&#125;<br></code></pre></td></tr></table></figure><p>后来，相关讨论移到了<a href="https://github.com/dotnet/csharplang/discussions/5498">dotnet/csharplang - Exploration: Roles, extension interfaces and static interface members</a>，出现了一个新的概念：<strong>角色</strong> <em>(Role)</em>，它是一个特殊的类型，可以被其他类型实现，从而为它们提供额外的功能。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">DataObject</span><br>&#123;<br>    <span class="hljs-keyword">public</span> DataObject <span class="hljs-keyword">this</span>[<span class="hljs-built_in">string</span> index] &#123; <span class="hljs-keyword">get</span>; &#125; <span class="hljs-comment">// throw if not found</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> ID &#123; <span class="hljs-keyword">get</span>; &#125;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Reload</span>()</span>;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> <span class="hljs-title">AsString</span>()</span>; <span class="hljs-comment">// throw if the DataObject does not represent a string</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> IEnumerable&lt;DataObject&gt; <span class="hljs-title">AsEnumerable</span>()</span>; <span class="hljs-comment">// throw if not...</span><br>    ...<br>&#125;<br><br><span class="hljs-keyword">public</span> role Order of DataObject<br>&#123;<br>    <span class="hljs-keyword">public</span> Customer Customer =&gt; <span class="hljs-keyword">this</span>[<span class="hljs-string">&quot;Customer&quot;</span>];<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> Description =&gt; <span class="hljs-keyword">this</span>[<span class="hljs-string">&quot;Description&quot;</span>].AsString();<br>    ...<br>&#125;<br><span class="hljs-keyword">public</span> role Customer of DataObject<br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> Name =&gt; <span class="hljs-keyword">this</span>[<span class="hljs-string">&quot;Name&quot;</span>].AsString();<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> Address =&gt; <span class="hljs-keyword">this</span>[<span class="hljs-string">&quot;Address&quot;</span>].AsString();<br>    <span class="hljs-keyword">public</span> IEnumerable&lt;Order&gt; Orders =&gt; <span class="hljs-keyword">this</span>[<span class="hljs-string">&quot;Orders&quot;</span>].AsEnumerable();<br>    ...<br>&#125;<br><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">CommerceFramework</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> IEnumerable&lt;Customer&gt; <span class="hljs-title">LoadCustomers</span>()</span>;<br>    ...<br>&#125;<br></code></pre></td></tr></table></figure><p>该机制讨论截止 2026/04/02 时查看，停留在 2023 年。</p><hr /><p>近期的 C# 13 预览版本中，出现了隐式扩展成员的特性，允许开发者在不修改原有类型定义的情况下，为它们添加新的成员：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-comment">// 目前</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">StringExtensions</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-built_in">int</span> <span class="hljs-title">WordCount</span>(<span class="hljs-params"><span class="hljs-keyword">this</span> <span class="hljs-built_in">string</span> str</span>)</span><br>    &#123;<br>        <span class="hljs-keyword">return</span> str.Split(<span class="hljs-string">&#x27; &#x27;</span>, StringSplitOptions.RemoveEmptyEntries).Length;<br>    &#125;<br>&#125;<br><br><span class="hljs-built_in">string</span> text = <span class="hljs-string">&quot;Hello, world!&quot;</span>;<br><span class="hljs-built_in">int</span> count = text.WordCount(); <span class="hljs-comment">// 直接调用扩展方法</span><br><br><span class="hljs-comment">// C# 13 预览版本</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">implicit</span> extension StringExtension <span class="hljs-keyword">for</span> <span class="hljs-built_in">string</span><br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> WordCount =&gt; <span class="hljs-keyword">this</span>.Split(<span class="hljs-string">&#x27; &#x27;</span>, StringSplitOptions.RemoveEmptyEntries).Length;<br>&#125;<br><br><span class="hljs-built_in">string</span> text = <span class="hljs-string">&quot;Hello, world!&quot;</span>;<br><span class="hljs-built_in">int</span> count = text.WordCount; <span class="hljs-comment">// 直接访问扩展属性</span><br></code></pre></td></tr></table></figure><p>但是这个语法被推迟了，因为相关的 C# 语言设计会议：</p><ol><li><p>在 June 12th, 2024 中：</p><ul><li><code>Unsafe.As</code> 方案<ul><li>原计划使用 <code>Unsafe.As</code> 进行底层类型转换的代码生成策略被认为是<strong>不安全</strong>的。</li><li>在极端性能场景下，目前的策略可能会引发内存别名问题，导致 JIT 产生混淆。</li><li>如果要等到运行时提供更安全的 API 来支持这种模式，可能需要等到 .NET 11 或更晚。</li></ul></li><li>作为 Fallback 的语法糖方案<ul><li>思路：退回到将扩展类型视为<strong>静态方法的语法糖</strong>（类似现有的扩展方法）。</li><li>优势：有望赶上 .NET 9 推出预览版，且有机会与现有的扩展方法保持<strong>二进制兼容</strong>。</li><li>挑战：编译器内部的模型转换极其复杂，需要大量重写成员体（特别是处理<strong>嵌套闭包</strong>时），以模拟实例方法的语义。</li></ul></li></ul></li><li><p>在 June 26th, 2024 中：</p><ul><li>跨语言与兼容性的冲突<ul><li>选项一：强制其他语言/旧版编译器必须了解新特性（通过添加底层标记屏蔽调用），这意味着<strong>放弃向后兼容</strong>，旧代码无法平滑迁移。</li><li>选项二：允许旧编译器继续以<strong>静态方法</strong>的形式调用新扩展。优势是可以完美兼容旧代码，但代价是 C# 未来必须永久支持这种调用格式，否则就是破坏性变更。</li></ul></li><li>拆分处理策略<ul><li>非实例成员（属性、事件等新扩展）：坚决不允许以静态形式调用，其他语言必须显式支持。</li><li>实例方法（类似现有的 <code>this</code> 扩展）：强烈倾向于<strong>保留向后兼容性</strong>，方便旧的静态类平滑升级为扩展类型。</li></ul></li><li>兼容性带来的解析挑战<ul><li>编译器必须同时支持 <code>instance.M()</code> 和 <code>E.M(instance)</code> 两种语法。</li><li>新的“扩展类型”解析逻辑需要翻修，以对齐旧的“扩展方法”机制。</li><li>签名歧义（Ambiguity）：如果扩展类型内同时存在无参实例方法 <code>void M()</code> 和带参静态方法 <code>static void M(Instance i)</code>，当用户使用 <code>E.M(instance)</code> 调用时，编译器将难以抉择。</li></ul></li></ul></li></ol><p>最后，C# 14 添加了<strong>扩展块</strong>的特性，允许开发者在此处声明扩展属性：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Enumerable</span><br>&#123;<br>    extension&lt;TSource&gt;(IEnumerable&lt;TSource&gt; source)<br>    &#123;<br>        <span class="hljs-comment">// 扩展属性：</span><br>        <span class="hljs-keyword">public</span> <span class="hljs-built_in">bool</span> IsEmpty =&gt; !source.Any();<br><br>        <span class="hljs-comment">// 扩展方法：</span><br>        <span class="hljs-function"><span class="hljs-keyword">public</span> IEnumerable&lt;TSource&gt; <span class="hljs-title">Where</span>(<span class="hljs-params">Func&lt;TSource, <span class="hljs-built_in">bool</span>&gt; predicate</span>)</span> &#123; ... &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h2><p><strong>接口</strong>从类的角度定义了一个契约，<strong>Mixin</strong>则是从功能的角度提供了代码复用的机制，而<strong>Trait</strong>则试图将两者结合起来，既提供了契约又提供了实现。</p><p>从 C# 的发展历程来看，接口、默认接口实现、扩展方法、静态抽象成员、有状态的 Mixin 以及源生成器等特性，都在不断地向 Trait 的方向演进，试图在保持语言简洁性的同时，提供更强大的代码复用和抽象能力。</p>]]>
    </content>
    <id>https://ziling.moe/2026/dotnet-oop-interface-mixin-trait/</id>
    <link href="https://ziling.moe/2026/dotnet-oop-interface-mixin-trait/"/>
    <published>2026-04-02T08:00:00.000Z</published>
    <summary>在继承之外，各语言拿出了不同的机制来解决类之间的横向关系问题。</summary>
    <title>.NET 面向对象 - 接口与 Mixin 与 Trait</title>
    <updated>2026-04-02T08:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term=".NET Learning" scheme="https://ziling.moe/categories/NET-Learning/"/>
    <category term=".NET" scheme="https://ziling.moe/tags/NET/"/>
    <category term="面向对象" scheme="https://ziling.moe/tags/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1/"/>
    <category term="OOP" scheme="https://ziling.moe/tags/OOP/"/>
    <content>
      <![CDATA[<p>在面向对象 <em>(OOP)</em> 的三大特性之外，<strong>接口</strong> <em>(Interface)</em> 构成了一种契约。</p><p>C# 的单一继承虽然解决了多重继承带来的复杂性，但也限制了类的设计，难以设计多层级的多态结构。好在 C# 接口的多重实现（一个类能实现多个接口）弥补了这些部分。</p><h2 id="模型"><a class="markdownIt-Anchor" href="#模型"></a> 模型</h2><p>接口本质上是一种<strong>契约</strong> <em>(Contract)</em>，它保证了所有实现该接口的类都具备相同的能力。</p><p>原本的接口只是在<strong>定义</strong>一些方法、属性、事件等成员，同时绝对<strong>不提供</strong>任何具体实现细节，后者都是类来完成的。</p><p>从这个角度就能看出它和继承的区别：继承是<strong>is-a</strong>关系，接口是<strong>can-do</strong>关系。因为很多方法并不在乎你是谁，而在乎你能做什么。</p><p>这种设计思路同样赋予 OOP 原本自上而下的层级到了一个新的抽象：比如说，<code>IDisposable</code> 接口就定义了一个 <code>Dispose()</code> 方法，任何实现了这个接口的类都能被当作可释放资源来处理，而每个类对于如何释放自己的资源则有自己的实现细节。这种<strong>横切</strong>的感觉使得接口能够让整个系统的设计变得更低耦合，更高内聚。</p><p>同时，接口的多重实现机制比类的多重继承更安全。传统的接口不允许成员实现，也不保存状态，因此在实现多个接口的时候，不会出现成员冲突和状态不一致的问题。以二义性来促使系统设计其实是不如在编译期就捋清楚这一切的。</p><p>对于接口与抽象类，可以参阅<a href="/2026/dotnet-oop-encapsulation-inheritance-polymorphism/#%E6%8E%A5%E5%8F%A3%E4%B8%8E%E6%8A%BD%E8%B1%A1%E7%B1%BB">这篇</a>。</p><h2 id="实现"><a class="markdownIt-Anchor" href="#实现"></a> 实现</h2><p>C# 的接口可以分为<strong>隐式</strong>和<strong>显式</strong>两种实现方式。</p><p>通常，我们接触到的接口实现都是隐式的，即直接在类中实现接口成员，这样接口成员就成为了类的公共成员，可以通过类的实例来访问。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">interface</span> <span class="hljs-title">IAnimal</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">Speak</span>()</span>; <span class="hljs-comment">// 定义动物是会说话的</span><br>&#125;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title">Dog</span> : <span class="hljs-title">IAnimal</span> <span class="hljs-comment">// 狗被视为动物</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Speak</span>() <span class="hljs-comment">// 所以狗也必须实现说话的能力</span></span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;Woof!&quot;</span>); <span class="hljs-comment">// 狗的说话方式是 Woof!</span><br>    &#125;<br>&#125;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title">Zoo</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">MakeAnimalSpeak</span>(<span class="hljs-params">IAnimal animal</span>)</span><br>    &#123;<br>        animal.Speak(); <span class="hljs-comment">// 只要是动物，就能说话，不管是狗还是其他动物</span><br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>在某些情况下，可能会遇到接口成员与类成员同名的情况，这时就需要使用显式接口实现来解决二义性问题。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">interface</span> <span class="hljs-title">IControl</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">Paint</span>()</span>; <span class="hljs-comment">// 定义控件是可以被绘制的</span><br>&#125;<br><br><span class="hljs-keyword">interface</span> <span class="hljs-title">IWidget</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">Paint</span>()</span>; <span class="hljs-comment">// 定义小部件也是可以被绘制的</span><br>&#125;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title">Button</span> : <span class="hljs-title">IControl</span>, <span class="hljs-title">IWidget</span> <span class="hljs-comment">// 按钮既是控件也是小部件</span><br>&#123;<br>    <span class="hljs-keyword">void</span> IControl.Paint() <span class="hljs-comment">// 显式实现 IControl 的 Paint 方法</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;Painting control...&quot;</span>); <span class="hljs-comment">// 绘制控件的方式</span><br>    &#125;<br><br>    <span class="hljs-keyword">void</span> IWidget.Paint() <span class="hljs-comment">// 显式实现 IWidget 的 Paint 方法</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;Painting widget...&quot;</span>); <span class="hljs-comment">// 绘制小部件的方式</span><br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>显式实现看起来只是在类里面注明了我在实现哪个接口的哪个方法，但这个<strong>区分</strong>的意义不止这个。</p><p>首先，显式实现的接口成员<strong>不成为类的公共成员</strong>，只能通过接口类型来访问：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs csharp">Button button = <span class="hljs-keyword">new</span> Button();<br>button.Paint(); <span class="hljs-comment">// 编译错误，Button 类没有公共的 Paint 方法</span><br>IControl control = button;<br>control.Paint(); <span class="hljs-comment">// 调用 IControl 的 Paint 方法</span><br>IWidget widget = button;<br>widget.Paint(); <span class="hljs-comment">// 调用 IWidget 的 Paint 方法</span><br></code></pre></td></tr></table></figure><p>在设计时，IDE 的智能提示不会把显式实现的接口成员当作类的公共成员来提示，这样开发者就能保持 API 本身的纯净，避免混淆。</p><h2 id="实例"><a class="markdownIt-Anchor" href="#实例"></a> 实例</h2><p>.NET C# 中有很多实用且强大的接口。</p><h3 id="迭代器"><a class="markdownIt-Anchor" href="#迭代器"></a> 迭代器</h3><p>我们往往在处理数据集的时候使用 LINQ，这时就会用到 <code>IEnumerable&lt;T&gt;</code> 和 <code>IEnumerator&lt;T&gt;</code> 这两个接口。</p><p><code>IEnumerable&lt;T&gt;</code> 是声明该集合可以被枚举的契约，实现了该接口的类就可以使用 <code>foreach</code> 循环来遍历集合中的元素。</p><p>观察源代码：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IEnumerable</span>&lt;<span class="hljs-keyword">out</span> <span class="hljs-title">T</span>&gt; : <span class="hljs-title">IEnumerable</span><br>       <span class="hljs-keyword">where</span> <span class="hljs-title">T</span> : <span class="hljs-title">allows</span> <span class="hljs-title">ref</span> <span class="hljs-title">struct</span><br>&#123;<br>    <span class="hljs-keyword">new</span> <span class="hljs-function">IEnumerator&lt;T&gt; <span class="hljs-title">GetEnumerator</span>()</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>它只是声明了一个 <code>GetEnumerator()</code> 方法，返回一个 <code>IEnumerator&lt;T&gt;</code> 的实例，而具体的枚举逻辑则由实现这个接口的类来完成。</p><p>那<code>IEnumerator&lt;T&gt;</code> 又是什么呢？它是一个迭代器接口，定义了迭代器的基本操作，包括获取当前元素、移动到下一个元素以及重置迭代器等方法。所有想要自己实现<code>IEnumerable&lt;T&gt;</code> 的类都必须提供一个 <code>IEnumerator&lt;T&gt;</code> 的实现来支持迭代器的功能。</p><p>这样 C# 就能从语言层面上支持迭代器的概念，让开发者能够自定义自己的集合类型，并且能够使用 <code>foreach</code> 循环来遍历。</p><h3 id="资源释放"><a class="markdownIt-Anchor" href="#资源释放"></a> 资源释放</h3><p>尽管 C# 的垃圾回收器足够高效，但对于<strong>非托管</strong> <em>(Unmanaged)</em> 资源的释放，还是需要开发者来管理的。这个手动管理的过程就需要 <code>IDisposable</code> 接口来提供一个统一的契约。</p><p>GC 对于包括文件操作、数据库连接、网络连接等在内的非托管资源是无能为力的，这些资源需要开发者来显式地释放，否则就会导致资源泄漏，甚至可能引发性能问题。</p><p>比如<code>System.IO.FileStream</code>（采用<code>OSFileStreamStrategy</code>的实现）：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">sealed</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Dispose</span>(<span class="hljs-params"><span class="hljs-built_in">bool</span> disposing</span>)</span><br>&#123;<br>    <span class="hljs-keyword">if</span> (disposing &amp;&amp; _fileHandle != <span class="hljs-literal">null</span> &amp;&amp; !_fileHandle.IsClosed)<br>    &#123;<br>        _fileHandle.ThreadPoolBinding?.Dispose();<br>        _fileHandle.Dispose();<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>文件流被释放时，<code>Dispose</code> 方法会被调用来释放底层的文件句柄资源，确保系统资源得到回收。<em>这样别的软件就能继续使用这些资源了。</em></p><h2 id="协变与逆变"><a class="markdownIt-Anchor" href="#协变与逆变"></a> 协变与逆变</h2><p><code>Apple</code>是<code>Fruit</code>的子类，那么<code>IEnumerable&lt;Apple&gt;</code>在逻辑上可以被视为<code>IEnumerable&lt;Fruit&gt;</code>，因为苹果也是水果。</p><p>我们再次观察<code>IEnumerable&lt;T&gt;</code>的定义，里面并不是简单的<code>&lt;T&gt;</code>，而是<code>&lt;out T&gt;</code>，这就是<strong>协变</strong> <em>(Covariance)</em> 的标记。</p><blockquote><p>An object that is instantiated with a more derived type argument is assigned to an object instantiated with a less derived type argument.</p></blockquote><p>一个对象被实例化时使用了一个相对子类 <em>(More derived)</em> 的类型参数，并被赋值给一个使用了一个相对父类 <em>(Less derived)</em> 的类型参数的对象。</p><p>协变允许我们在接口中使用<strong>输出</strong>类型参数时，保持类型安全的同时实现类型的兼容性。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs csharp">IEnumerable&lt;Apple&gt; apples = <span class="hljs-keyword">new</span> List&lt;Apple&gt;();<br><span class="hljs-comment">// 可以赋值给父类，因为 IEnumerable&lt;T&gt; 是协变的</span><br>IEnumerable&lt;Fruit&gt; fruits = apples;<br></code></pre></td></tr></table></figure><p>协变会保留赋值的兼容性，而逆变则会反转这个过程：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">SetObject</span>(<span class="hljs-params"><span class="hljs-built_in">object</span> o</span>)</span> &#123; &#125;<br><br>Action&lt;<span class="hljs-built_in">object</span>&gt; actObject = SetObject;<br><span class="hljs-comment">// 可以将 Action&lt;object&gt; 赋值给 Action&lt;string&gt;，因为 Action&lt;T&gt; 是逆变的  </span><br>Action&lt;<span class="hljs-built_in">string</span>&gt; actString = actObject;<br></code></pre></td></tr></table></figure><p><code>Action&lt;T&gt;</code> 是一个<strong>逆变</strong> <em>(Contravariant)</em> 的委托类型，以<code>&lt;in T&gt;</code>的形式出现。</p><blockquote><p>An object that is instantiated with a less derived type argument is assigned to an object instantiated with a more derived type argument.</p></blockquote><p>一个对象被实例化时使用了一个相对父类 <em>(Less derived)</em> 的类型参数，并被赋值给一个使用了一个相对子类 <em>(More derived)</em> 的类型参数的对象。</p><p>它允许我们将一个接受 <code>object</code> 参数的委托赋值给一个接受 <code>string</code> 参数的委托，因为 <code>string</code> 是 <code>object</code> 的子类。</p><p><em>可以前往<a href="https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Action.cs,486d58da4553e12d">Action的源代码</a>观赏登神长阶</em></p><hr /><p>协变操作不是类型安全的，因为它允许我们将一个更具体的类型赋值给一个更抽象的类型，这可能会导致在运行时出现类型错误。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-built_in">object</span>[] array = <span class="hljs-keyword">new</span> String[<span class="hljs-number">10</span>];  <br>array[<span class="hljs-number">0</span>] = <span class="hljs-number">10</span>; <span class="hljs-comment">// 抛出 ArrayTypeMismatchException</span><br></code></pre></td></tr></table></figure><p>这里不安全是因为从<code>string[]</code>拿元素当作<code>object</code>来用是没问题的，但如果我们试图把一个<code>int</code>放到这个数组里，由于这会破坏<code>string[]</code>的内存结构，就会抛出<code>ArrayTypeMismatchException</code>异常。</p><p>当然，这是一个历史遗留问题，自从<code>in</code>和<code>out</code>关键字引入之后，C# 的泛型接口和委托就能够在编译时就保证类型安全。</p><p>然而逆变操作是类型安全的，它允许将一个更抽象的类型赋值给一个更具体的类型，因为在这种情况下，所有的操作都是合法的。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs csharp">Action&lt;<span class="hljs-built_in">object</span>&gt; actObject = (obj) =&gt; &#123;<br>    Console.WriteLine(<span class="hljs-string">&quot;对象: &quot;</span> + obj.ToString());<br>&#125;;<br><br><span class="hljs-comment">// 逆变！</span><br>Action&lt;<span class="hljs-built_in">string</span>&gt; actString = actObject; <br>actString(<span class="hljs-string">&quot;Hello World&quot;</span>); <span class="hljs-comment">// 没问题</span><br></code></pre></td></tr></table></figure><p>如果一个人能吃所有的水果，那么他也能吃苹果。</p><hr /><p>当然，如果既没有<code>in</code>也没有<code>out</code>，那么这个接口就是<strong>不变</strong> <em>(Invariant)</em> 的了，这时就不能进行类型转换了：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs csharp">List&lt;<span class="hljs-built_in">string</span>&gt; strings = <span class="hljs-keyword">new</span> List&lt;<span class="hljs-built_in">string</span>&gt;();<br><span class="hljs-comment">// 不能赋值给 List&lt;object&gt;，因为 List&lt;T&gt; 是不变的</span><br>List&lt;<span class="hljs-built_in">object</span>&gt; objects = strings; <span class="hljs-comment">// 编译错误</span><br></code></pre></td></tr></table></figure><h2 id="默认接口实现"><a class="markdownIt-Anchor" href="#默认接口实现"></a> 默认接口实现</h2><p>当一个接口被很多很多类实现了之后，如果我们想要在接口中添加一个新的方法，那么就会面临一个问题：所有实现了这个接口的类都必须提供这个新方法的实现，否则就会导致编译错误。</p><p>这个很容易引发巨型 Breaking Change 的问题在 C# 8.0 引入了<strong>默认接口实现</strong> <em>(Default Interface Implementation)</em> 的特性后得到了很好的解决。</p><h3 id="影响"><a class="markdownIt-Anchor" href="#影响"></a> 影响</h3><p>说实话，这玩意对于传统继承和接口的职责划分来说是有点模糊的了。但是，它确实提供了一种在不破坏现有实现的前提下扩展接口功能的方式。</p><ul><li>在多重实现和默认实现的基础上，接口的功能已经非常接近于<strong>Trait</strong>了。在架构中，多个完全不相关、无共同祖先的类可以通过实现同一个接口来共享一些行为，及其安全地<strong>混入</strong> <em>(Mixin)</em> 到这些类中。在必要时通过重写接口的默认实现来定制行为。</li><li>接口虽然可以提供方法的默认实现，但它仍然不能包含任何状态（字段）。这是因为接口的设计初衷是为了定义行为的契约，而不是存储数据。</li><li>接口的默认实现不能隐式地注入到实现类的公共 API 中，调用默认实现的方法必须通过接口类型来访问。</li></ul><p>对于第三个：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">interface</span> <span class="hljs-title">IGreeter</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">Greet</span>()</span> =&gt; Console.WriteLine(<span class="hljs-string">&quot;Hello!&quot;</span>); <span class="hljs-comment">// 默认实现</span><br>&#125;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span> : <span class="hljs-title">IGreeter</span><br>&#123;<br>    <span class="hljs-comment">// 没有重写 Greet 方法，所以会使用接口的默认实现</span><br>&#125;<br><br>Person person = <span class="hljs-keyword">new</span> Person();<br>person.Greet(); <span class="hljs-comment">// 编译错误，Person 类没有公共的 Greet 方法</span><br>((IGreeter)person).Greet(); <span class="hljs-comment">// 通过接口类型访问默认实现的方法</span><br></code></pre></td></tr></table></figure><h2 id="静态抽象成员"><a class="markdownIt-Anchor" href="#静态抽象成员"></a> 静态抽象成员</h2><p>在 C# 11 之前，接口存在一个局限性：它只能定义<strong>实例</strong>成员的契约，而对静态成员（如静态方法、运算符重载、静态工厂）无能为力。这就导致我们无法通过接口来强制要求一个类必须提供某个特定的静态方法或运算符。</p><p>而<strong>静态抽象成员</strong> <em>(Static Abstract Members in Interfaces)</em> 彻底打破了这个限制，这也是实现<strong>泛型数学</strong> <em>(Generic Math)</em> 的关键。</p><h3 id="概念"><a class="markdownIt-Anchor" href="#概念"></a> 概念</h3><p>如果我们想写一个泛型方法来计算一组数字的和，会发现：泛型 <code>&lt;T&gt;</code> 并不知道自己是否支持 <code>+</code> 运算符。</p><p>通过引入静态抽象成员，接口现在可以定义静态的属性、方法甚至运算符作为契约。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IAddable</span>&lt;<span class="hljs-title">T</span>&gt; <span class="hljs-keyword">where</span> <span class="hljs-title">T</span> : <span class="hljs-title">IAddable</span>&lt;<span class="hljs-title">T</span>&gt;<br>&#123;<br>    <span class="hljs-comment">// 静态抽象运算符</span><br>    <span class="hljs-keyword">static</span> <span class="hljs-keyword">abstract</span> T <span class="hljs-keyword">operator</span> +(T left, T right);<br>    <br>    <span class="hljs-comment">// 静态抽象属性，比如零</span><br>    <span class="hljs-keyword">static</span> <span class="hljs-keyword">abstract</span> T Zero &#123; <span class="hljs-keyword">get</span>; &#125;<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="应用"><a class="markdownIt-Anchor" href="#应用"></a> 应用</h3><p>当一个类实现这个接口时，它必须提供这些静态成员的具体实现：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">struct</span> MyNumber : IAddable&lt;MyNumber&gt;<br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> Value &#123; <span class="hljs-keyword">get</span>; &#125;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">MyNumber</span>(<span class="hljs-params"><span class="hljs-built_in">int</span> <span class="hljs-keyword">value</span></span>)</span> =&gt; Value = <span class="hljs-keyword">value</span>;<br><br>    <span class="hljs-comment">// 实现静态抽象运算符</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> MyNumber <span class="hljs-keyword">operator</span> +(MyNumber left, MyNumber right)<br>    &#123;<br>        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> MyNumber(left.Value + right.Value);<br>    &#125;<br><br>    <span class="hljs-comment">// 实现静态抽象属性</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> MyNumber Zero =&gt; <span class="hljs-keyword">new</span> MyNumber(<span class="hljs-number">0</span>);<br>&#125;<br></code></pre></td></tr></table></figure><p>之后你可以直接通过泛型类型参数 <code>T</code> 来调用这些静态成员，而不需要任何实例：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-comment">// 约束：T 一定有 + 运算符和 Zero 属性</span><br><span class="hljs-function"><span class="hljs-keyword">static</span> T <span class="hljs-title">Sum</span>&lt;<span class="hljs-title">T</span>&gt;(<span class="hljs-params">IEnumerable&lt;T&gt; values</span>) <span class="hljs-keyword">where</span> T : IAddable&lt;T&gt;</span><br>&#123;<br>    T result = T.Zero; <span class="hljs-comment">// 直接调用静态属性</span><br>    <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> values)<br>    &#123;<br>        result += item; <span class="hljs-comment">// 直接使用运算符</span><br>    &#125;<br>    <span class="hljs-keyword">return</span> result;<br>&#125;<br><br><span class="hljs-keyword">var</span> numbers = <span class="hljs-keyword">new</span>[] &#123; <span class="hljs-keyword">new</span> MyNumber(<span class="hljs-number">1</span>), <span class="hljs-keyword">new</span> MyNumber(<span class="hljs-number">2</span>), <span class="hljs-keyword">new</span> MyNumber(<span class="hljs-number">3</span>) &#125;;<br>MyNumber total = Sum(numbers); <br>Console.WriteLine(total.Value); <span class="hljs-comment">// 6</span><br></code></pre></td></tr></table></figure><h2 id="去虚化"><a class="markdownIt-Anchor" href="#去虚化"></a> 去虚化</h2><p>传统的接口需要在运行时通过<strong>虚方法表</strong> <em>(VTable)</em> 来进行动态分派，这导致 JIT 无法对方法体进行内联优化，从而引入了性能开销。</p><p>然而，当我们结合<strong>泛型约束</strong> <em>(Generic Constraints)</em> 和值类型时，JIT 能够执行一种称为<strong>去虚化</strong> <em>(Devirtualization)</em> 的优化。</p><p>当我们把实现了接口的结构体传递给带有泛型约束的方法时（例如 <code>void Process&lt;T&gt;(T item) where T : IInterface</code>），JIT 会为这个特定的值类型生成一份专属的机器码。由于在编译期具体的类型已经完全确定，JIT 就能完全绕过虚方法表，将原本的接口虚方法调用直接转换为<strong>直接调用</strong> <em>(Direct Call)</em>。</p><h3 id="实例-2"><a class="markdownIt-Anchor" href="#实例-2"></a> 实例</h3><p>对于下面的代码：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">interface</span> <span class="hljs-title">IProcessor</span> <br>&#123; <br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">Process</span>()</span>; <br>&#125;<br><br><span class="hljs-keyword">struct</span> FastProcessor : IProcessor <br>&#123; <br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Process</span>()</span> &#123; <span class="hljs-comment">/* 逻辑 */</span> &#125; <br>&#125;<br><br><span class="hljs-comment">// 泛型约束 -&gt; 去虚化</span><br><span class="hljs-function"><span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Run</span>&lt;<span class="hljs-title">T</span>&gt;(<span class="hljs-params">T processor</span>) <span class="hljs-keyword">where</span> T : IProcessor</span><br>&#123;<br>    processor.Process();<br>&#125;<br><br>Run(<span class="hljs-keyword">new</span> FastProcessor());<br></code></pre></td></tr></table></figure><p>检查 IL 代码，我们会发现：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs plaintext">IL_0000nop<br>IL_0001ldarga.s00 <br>IL_0003constrained.UserQuery.T<br>IL_0009callvirtIProcessor.Process ()<br>IL_000Enop<br>IL_000Fret<br></code></pre></td></tr></table></figure><p>为什么还有个<code>callvirt</code>？因为去虚化是到了 JIT 编译阶段才发生的优化，IL 代码中仍然是一个接口调用的形式。那 JIT 又是怎么知道它可以被去虚化的呢？</p><p>我们会发现 IL 中还有个 <code>constrained.</code> 指令，这个指令仅作为<code>callvirt</code>的前缀存在。而存在时，会进行如下检查：</p><ul><li>如果 <code>T</code> 是<strong>引用类型</strong>，那么它会取消引用，并像普通对象一样正常进行虚方法调用。</li><li>如果 <code>T</code> 是<strong>值类型，且实现了</strong> 该调用的方法（比如<code>IProcessor.Process()</code>），那么它会直接调用该方法的实例实现，绕过虚方法表。</li><li>如果 <code>T</code> 是<strong>值类型，但没有实现</strong> 该调用的方法，那么它会被<strong>装箱</strong> <em>(Boxing)</em>，然后进行虚方法调用。</li></ul><p>上述例子会进入第二个情况，从而达到零装箱无多态开销的效果。</p>]]>
    </content>
    <id>https://ziling.moe/2026/dotnet-oop-interface/</id>
    <link href="https://ziling.moe/2026/dotnet-oop-interface/"/>
    <published>2026-03-20T07:00:00.000Z</published>
    <summary>接口是一个契约</summary>
    <title>.NET 面向对象 - 接口</title>
    <updated>2026-03-20T07:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term=".NET Learning" scheme="https://ziling.moe/categories/NET-Learning/"/>
    <category term=".NET" scheme="https://ziling.moe/tags/NET/"/>
    <category term="面向对象" scheme="https://ziling.moe/tags/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1/"/>
    <category term="OOP" scheme="https://ziling.moe/tags/OOP/"/>
    <content>
      <![CDATA[<p>.NET 作为面向对象老资历之一，其生态系统的架构设计也深度依赖于 OOP 范式。.NET 提供了蓝图来规定各个实体的能力、存储方式和行为。</p><h2 id="回顾定义"><a class="markdownIt-Anchor" href="#回顾定义"></a> 回顾定义</h2><p>让我们从最基础的概念 —— <strong>类</strong> <em>(Class)</em> 和 <strong>对象</strong> <em>(Object)</em> 开始。</p><p>当我们在一个面向对象为范式的编程语言中对 <strong>抽象</strong> <em>(Abstraction)</em> 的定义进行建模的时候，比如一个学生应当是什么样的，有哪些属性和行为，我们就会定义一个类。类是一个<strong>蓝图</strong>，描述了对象的结构和行为。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">Student</span><br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> Name &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> Age &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Study</span>()</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">$&quot;<span class="hljs-subst">&#123;Name&#125;</span> is studying.&quot;</span>);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>也就是说，类本身其实什么也没做，它只是一个模板。我们需要通过<strong>实例化</strong> <em>(Instantiation)</em> 这个类来创建一个对象，这个对象才是真正的实体，拥有实际的数据和行为。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">var</span> Misaka = <span class="hljs-keyword">new</span> Student &#123; Name = <span class="hljs-string">&quot;Misaka&quot;</span>, Age = <span class="hljs-number">14</span> &#125;;<br>Misaka.Study(); <span class="hljs-comment">// 输出: Misaka is studying.</span><br></code></pre></td></tr></table></figure><div class="tag-plugin image"><div class="image-bg" style="width:100%;"><img class="lazy" src="/images/2026/dotnet-oop-encapsulation-inheritance-polymorphism/sisters.webp" data-src="/images/2026/dotnet-oop-encapsulation-inheritance-polymorphism/sisters.webp" alt="「シスターズ」&nbsp;妹達" data-fancybox="true" style="width:400px;"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">「シスターズ」&nbsp;妹達</span></div></div><h2 id="三大特性"><a class="markdownIt-Anchor" href="#三大特性"></a> 三大特性</h2><p>在抽象之上，OOP 同样提供了三个重要的特性来帮助我们更好地组织和管理代码：<strong>封装</strong> <em>(Encapsulation)</em>、<strong>继承</strong> <em>(Inheritance)</em> 和 <strong>多态</strong> <em>(Polymorphism)</em>。</p><p>C# 本身也随着版本更迭加入了诸如<strong>接口</strong> <em>(Interface)</em>、<strong>仅初始化属性</strong> <em>(Init-only properties)</em> 等特性来丰富 OOP 的表达能力。</p><h3 id="封装"><a class="markdownIt-Anchor" href="#封装"></a> 封装</h3><p>每个人心中都有秘密，一些信息可以让谁知道，从嘴里说出来的时候又会变成什么样子，这些都是封装的内容。</p><p>比如，我的年龄是<code>19</code>，但是我不希望别人能直接知道这个信息，我只能透露我是不是成年人。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span>(<span class="hljs-title">int</span> <span class="hljs-title">age</span>)<br>&#123;<br>    <span class="hljs-keyword">private</span> <span class="hljs-built_in">int</span> age = age; <span class="hljs-comment">// 年龄是私有的，外部无法直接访问</span><br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">IsAdult</span>()  <span class="hljs-comment">// 通过一个公共方法来判断是否是成年人</span></span><br>    &#123;<br>        <span class="hljs-keyword">return</span> age &gt;= <span class="hljs-number">18</span>;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><blockquote><p><strong>Encapsulation</strong> Hiding the internal state and functionality of an object and only allowing access through a public set of functions.</p></blockquote><p><strong>封装</strong> <em>(Encapsulation)</em> 是指将对象的状态（数据）和行为（方法）封装在一起，并通过<strong>访问修饰符</strong> <em>(Access Modifiers)</em> 来控制对这些成员的访问。这样可以保护对象的内部状态不被外部直接修改，从而提高代码的安全性和可维护性。</p><ul><li>状态（数据）通常通过<strong>字段</strong> <em>(Fields)</em> 或 <strong>属性</strong> <em>(Properties)</em> 来表示。</li><li>行为（方法）则通过<strong>方法</strong> <em>(Methods)</em> 来定义。</li><li>访问修饰符如 <code>private</code>, <code>public</code>, <code>protected</code> 等可以控制成员的可见性。</li></ul><h4 id="访问修饰符"><a class="markdownIt-Anchor" href="#访问修饰符"></a> 访问修饰符</h4><p>为了达到封装的目的，C# 提供了多种访问修饰符来控制类成员的访问权限。以下是一些常见的：</p><table><thead><tr><th>修饰符</th><th>访问权限</th><th>应用场景</th></tr></thead><tbody><tr><td><code>public</code></td><td>任何地方都可以访问</td><td>定义 API 等需要被外部使用的成员</td></tr><tr><td><code>internal</code></td><td>只能在同一<strong>程序集</strong>内访问</td><td>构建库内部核心逻辑、辅助方法等</td></tr><tr><td><code>protected</code></td><td>只能在类及其<strong>派生类</strong>中访问</td><td>后面的<strong>继承</strong>部分会详细介绍</td></tr><tr><td><code>private</code></td><td>只能在类内部访问</td><td>定义类的内部状态，保护数据不被外部直接修改</td></tr></tbody></table><p>可以构想一个小说里容易见到的家族场景，每个家族是被视为<code>internal</code>的，家族成员之间是<code>protected</code>的，而家族外的人只能通过<code>public</code>的方法来了解这个家族的情况，当然，家族中每个人的秘密则是<code>private</code>的。</p><details class="tag-plugin colorful folding" color="orange" open><summary><p>默认行为</p></summary><div class="body"><p>在 C# 中，如果没有显式指定访问修饰符，类成员默认是 <code>private</code> 的，而类本身则默认是 <code>internal</code> 的。这种防御性的默认设置有助于鼓励开发者在设计类时明确地考虑访问权限，从而更好地实现封装。</p> </div></details><p>当然，有些修饰符是可以组合使用的，也很容易混淆：</p><table><thead><tr><th>修饰符组合</th><th>访问权限</th></tr></thead><tbody><tr><td><code>protected internal</code></td><td>只能在同一程序集内<strong>或</strong>派生类中访问</td></tr><tr><td><code>private protected</code></td><td>只能在同一程序集内<strong>的</strong>派生类中访问</td></tr></tbody></table><p><code>protected internal</code> 通常在大型项目中使用，在允许自身程序集内的调用的同时，开发者也可以通过继承来扩展类的功能。而 <code>private protected</code> 则更为严格，只有在同一程序集内的派生类才能访问，这对于一些需要高度封装的内部实现非常有用。</p><p>C# 还引入了 <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/file"><code>file</code> 访问修饰符</a>，表示成员只能在同一<strong>源文件</strong>内访问，在进行<strong>源生成器</strong> <em>(Source Generators)</em> 开发时非常有用，可以将一些辅助方法或状态隐藏在同一文件中，而不暴露给其他文件。</p><h4 id="属性和字段"><a class="markdownIt-Anchor" href="#属性和字段"></a> 属性和字段</h4><p>在现代 C# 最佳实践中，暴露封装数据的标准方法是通过<strong>属性</strong> <em>(Properties)</em>，而不是直接暴露<strong>字段</strong> <em>(Fields)</em>。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">Example</span><br>&#123;<br>    <span class="hljs-keyword">private</span> <span class="hljs-built_in">int</span> _field; <span class="hljs-comment">// 字段</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> Property &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125; <span class="hljs-comment">// 属性</span><br>&#125;<br></code></pre></td></tr></table></figure><p><em>默认 <code>get</code> 和 <code>set</code> 的属性被称为自动属性 (Auto-implemented Properties)。</em></p><p>我们会发现相比于字段，属性多了一个访问器（<code>get</code> 和 <code>set</code>），这使得我们可以在访问属性时添加额外的逻辑，比如验证输入、触发事件等，从而更好地控制数据的访问和修改。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span><br>&#123;<br>    <span class="hljs-keyword">private</span> <span class="hljs-built_in">int</span> _age; <span class="hljs-comment">// 私有字段</span><br><br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> Age <span class="hljs-comment">// 公共属性</span><br>    &#123;<br>        <span class="hljs-keyword">get</span> =&gt; _age; <span class="hljs-comment">// 访问器：获取年龄</span><br>        <span class="hljs-keyword">set</span><br>        &#123;<br>            <span class="hljs-keyword">if</span> (<span class="hljs-keyword">value</span> &lt; <span class="hljs-number">0</span>) <span class="hljs-comment">// 验证输入</span><br>                <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> ArgumentException(<span class="hljs-string">&quot;Age cannot be negative.&quot;</span>);<br>            _age = <span class="hljs-keyword">value</span>; <span class="hljs-comment">// 设置年龄</span><br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>在最近 C# 14 中引入的<code>field</code>关键字，让字段的存在感进一步降低，开发者可以直接使用属性来定义数据成员，而不需要显式声明字段。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span><br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> Age &#123;<br>        <span class="hljs-keyword">get</span>;<br>        <span class="hljs-keyword">set</span> =&gt; field = (<span class="hljs-keyword">value</span> &gt;= <span class="hljs-number">0</span>)<br>            ? <span class="hljs-keyword">value</span><br>            : <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> ArgumentOutOfRangeException(<span class="hljs-keyword">nameof</span>(<span class="hljs-keyword">value</span>), <span class="hljs-string">&quot;Age cannot be negative.&quot;</span>);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>即使这样，字段依然是 C# 的基础数据成员类型，属性只是对字段的一层封装，编译器会将属性访问器转换为对字段的访问。</p><div class="tag-plugin image"><div class="image-bg" style="width:100%;"><img class="lazy" src="/images/2026/dotnet-oop-encapsulation-inheritance-polymorphism/property-get-set.webp" data-src="/images/2026/dotnet-oop-encapsulation-inheritance-polymorphism/property-get-set.webp" alt="编译器对属性访问器的处理" data-fancybox="true" style="width:400px;"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">编译器对属性访问器的处理</span></div></div><details class="tag-plugin colorful folding" color="blue" open><summary><p>命名规范</p></summary><div class="body"><p>在 C# 中，字段通常使用下划线前缀（例如 <code>_age</code>）来区分于属性，而属性则使用 PascalCase 命名法（例如 <code>Age</code>）。</p> <p>对于<code>bool</code>类型的属性，通常会使用 <code>Is</code> 或 <code>Has</code> 前缀来表示一个状态，例如 <code>IsAdult</code> 或 <code>HasLicense</code>，以提高代码的可读性。</p> </div></details><hr /><p><code>get</code> 和 <code>set</code> 可以只保留其中一个，如果只保留 <code>get</code>，则该属性为只读的，只能获取值而不能修改；如果只保留 <code>set</code>，则该属性为只写的，只能设置值而不能获取。</p><p>需要注意的是，<strong>不要把属性写成方法</strong>，因为一个是数据的表达，另一个是行为的表达，混淆了两者会带来严重的设计问题。这在只写属性的情况下尤其明显，因为它会让使用者感到困惑，不知道这个方法是用来获取数据还是设置数据。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-comment">// 错误的设计</span><br><span class="hljs-keyword">class</span> <span class="hljs-title">PasswordManager</span><br>&#123;<br>    <span class="hljs-keyword">private</span> <span class="hljs-built_in">string</span> _password;<br><br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> Password<br>    &#123;<br>        <span class="hljs-keyword">set</span> =&gt; _password = <span class="hljs-keyword">value</span>; <span class="hljs-comment">// 只写属性，设置密码</span><br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">VerifyPassword</span>(<span class="hljs-params"><span class="hljs-built_in">string</span> input</span>)</span><br>    &#123;<br>        <span class="hljs-keyword">return</span> input == _password; <span class="hljs-comment">// 验证密码的方法</span><br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><hr /><p>和 <code>get</code> 和 <code>set</code> 一样，从不可变性的角度触发，C# 还引入了 <code>init</code> 访问器，表示属性只能在对象初始化时设置一次，之后就变成只读的了。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span><br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> Name &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">init</span>; &#125; <span class="hljs-comment">// 只能在对象初始化时设置</span><br>&#125;<br><br><span class="hljs-keyword">var</span> person = <span class="hljs-keyword">new</span> Person &#123; Name = <span class="hljs-string">&quot;Alice&quot;</span> &#125;;<br>person.Name = <span class="hljs-string">&quot;Bob&quot;</span>; <span class="hljs-comment">// 无法修改 Name 属性</span><br></code></pre></td></tr></table></figure><h3 id="继承"><a class="markdownIt-Anchor" href="#继承"></a> 继承</h3><p><strong>一个学生先是一个人，然后才是学生。</strong></p><p>学生具有人的所有特征和行为，同时还具有学生特有的特征和行为，这就是继承的概念。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span><br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> Name &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> Age &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Eat</span>()</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">$&quot;<span class="hljs-subst">&#123;Name&#125;</span> is eating.&quot;</span>);<br>    &#125;<br>&#125;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title">Student</span> : <span class="hljs-title">Person</span> <span class="hljs-comment">// Student 继承自 Person</span><br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> School &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Study</span>()</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">$&quot;<span class="hljs-subst">&#123;Name&#125;</span> is studying at <span class="hljs-subst">&#123;School&#125;</span>.&quot;</span>);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><blockquote><p><strong>Inheritance</strong> Ability to create new abstractions based on existing abstractions.</p></blockquote><p><strong>继承</strong> <em>(Inheritance)</em> 是指一个类可以基于另一个类创建，新的类称为<strong>派生类</strong> <em>(Derived Class)</em> 或 <strong>子类</strong> <em>(Subclass)</em>，原来的类称为<strong>基类</strong> <em>(Base Class)</em> 或 <strong>父类</strong> <em>(Superclass)</em>。派生类继承了基类的成员（字段、属性、方法等），并且可以添加新的成员或重写基类的成员来实现特定的行为。</p><p>它本质上建立了严格的<strong>is-a</strong>关系（比如苹果是水果，学生是人）。我们也可以称苹果是水果的特化，学生是人的特化。而在代码中，如果一个方法能够处理水果相关的逻辑，那么它同样能够处理苹果相关的逻辑。</p><hr /><p>和 C++ 不同，C# 不支持多重继承（一个类只能有一个直接的基类），这样有效避免了恶心的<strong>菱形继承</strong>问题（Diamond Problem）：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Animal</span> &#123;<br><span class="hljs-keyword">public</span>:<br>    <span class="hljs-type">int</span> weight = <span class="hljs-number">10</span>;<br>&#125;;<br><br><span class="hljs-comment">// 虎和狮都继承了 Animal</span><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">Tiger</span> : <span class="hljs-keyword">public</span> Animal &#123;&#125;;<br><span class="hljs-keyword">class</span> <span class="hljs-title class_">Lion</span> : <span class="hljs-keyword">public</span> Animal &#123;&#125;;<br><br><span class="hljs-comment">// 狮虎兽 (Liger) 同时继承了两者</span><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">Liger</span> : <span class="hljs-keyword">public</span> Tiger, <span class="hljs-keyword">public</span> Lion &#123;&#125;;<br><br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>&#123;<br>    Liger liger;<br>    <span class="hljs-comment">// error: &#x27;weight&#x27; is ambiguous</span><br>    <span class="hljs-comment">// 你指的是 Tiger 里的 weight，还是 Lion 里的 weight？</span><br>    std::cout &lt;&lt; liger.weight &lt;&lt; std::endl; <br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre></td></tr></table></figure><hr /><p>继承是有传递性的，如果 <code>Student</code> 继承自 <code>Person</code>，而 <code>GraduateStudent</code> 继承自 <code>Student</code>，那么 <code>GraduateStudent</code> 也会继承自 <code>Person</code>。同样，在 .NET 中，所有的类最终都继承自 <code>System.Object</code> 类，这意味着所有的类都具有 <code>System.Object</code> 类定义的成员（如 <code>ToString()</code>, <code>Equals()</code>, <code>GetHashCode()</code> 等）。</p><details class="tag-plugin colorful folding" color="orange" open><summary><p>不被继承的</p></summary><div class="body"><p>在派生类继承基类时，类的 <strong>构造函数</strong> <em>(Constructor)</em> 和 <strong>析构函数</strong> <em>(Destructor)</em> 是不会被继承的。每个类都需要定义自己的构造函数来初始化对象的状态，而析构函数则用于在对象被垃圾回收时执行清理操作。</p> </div></details><p>若基类存在有参构造函数，派生类必须显式调用基类的构造函数来确保基类的成员得到正确初始化。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span><br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> Name &#123; <span class="hljs-keyword">get</span>; &#125;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> Age &#123; <span class="hljs-keyword">get</span>; &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Person</span>(<span class="hljs-params"><span class="hljs-built_in">string</span> name, <span class="hljs-built_in">int</span> age</span>)</span><br>    &#123;<br>        Name = name;<br>        Age = age;<br>    &#125;<br>&#125;<br><br><span class="hljs-comment">// 传统写法</span><br><span class="hljs-keyword">class</span> <span class="hljs-title">Student</span> : <span class="hljs-title">Person</span><br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> School &#123; <span class="hljs-keyword">get</span>; &#125;<br><br>    <span class="hljs-comment">// 借助 base 关键字调用基类的构造函数</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Student</span>(<span class="hljs-params"><span class="hljs-built_in">string</span> name, <span class="hljs-built_in">int</span> age, <span class="hljs-built_in">string</span> school</span>) : <span class="hljs-title">base</span>(<span class="hljs-params">name, age</span>)</span><br>    &#123;<br>        School = school;<br>    &#125;<br>&#125;<br><br><span class="hljs-comment">// 现代写法</span><br><span class="hljs-keyword">class</span> <span class="hljs-title">Student</span>(<span class="hljs-title">string</span> <span class="hljs-title">name</span>, <span class="hljs-title">int</span> <span class="hljs-title">age</span>, <span class="hljs-title">string</span> <span class="hljs-title">school</span>) : <span class="hljs-title">Person</span>(<span class="hljs-title">name</span>, <span class="hljs-title">age</span>)<br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> School &#123; <span class="hljs-keyword">get</span>; &#125; = school;<br>&#125;<br></code></pre></td></tr></table></figure><h4 id="抽象类"><a class="markdownIt-Anchor" href="#抽象类"></a> 抽象类</h4><p>如果说类是对象的蓝图，那么<strong>抽象类</strong>就是一个不完整的蓝图。</p><p>C# 引入了 <code>abstract</code> 关键字来定义抽象类，表示这个类不能被实例化，只能被继承。抽象类可以包含抽象方法（没有实现的方法），派生类必须实现这些抽象方法才能成为具体的类。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">abstract</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Animal</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">void</span> <span class="hljs-title">MakeSound</span>()</span>; <span class="hljs-comment">// 抽象方法，没有实现</span><br>&#125;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title">Dog</span> : <span class="hljs-title">Animal</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">void</span> <span class="hljs-title">MakeSound</span>() <span class="hljs-comment">// 实现抽象方法</span></span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;Woof!&quot;</span>);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><h5 id="接口与抽象类"><a class="markdownIt-Anchor" href="#接口与抽象类"></a> 接口与抽象类</h5><p>最早的时候，开发者对于接口和抽象类的使用其实很明确，接口作为一种契约或是协议，定义了一个类必须实现的成员，而抽象类则提供了一种半成品的实现，允许派生类继承和扩展。</p><p>然而，随着 C# 8 引入了<strong>默认接口方法</strong> <em>(Default Interface Methods)</em>，接口也可以提供方法的默认实现，这使得接口和抽象类之间的界限变得模糊了起来。这一特性的初衷是为了在不破坏现有接口的情况下添加新功能：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-comment">// 定义接口</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">ILogger</span><br>&#123;<br>    <span class="hljs-comment">// 类必须实现它</span><br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">Log</span>(<span class="hljs-params"><span class="hljs-built_in">string</span> message</span>)</span>;<br><br>    <span class="hljs-comment">// 默认接口方法</span><br>    <span class="hljs-comment">// 现有的实现类（如 ConsoleLogger）不需要修改代码也能直接“继承”这个功能</span><br>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">LogError</span>(<span class="hljs-params"><span class="hljs-built_in">string</span> message</span>)</span> <br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">$&quot;[ERROR]: <span class="hljs-subst">&#123;message&#125;</span>&quot;</span>);<br>    &#125;<br>&#125;<br><br><span class="hljs-comment">// 现有的实现类，只实现了 Log 方法</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">ConsoleLogger</span> : <span class="hljs-title">ILogger</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Log</span>(<span class="hljs-params"><span class="hljs-built_in">string</span> message</span>)</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">$&quot;[INFO]: <span class="hljs-subst">&#123;message&#125;</span>&quot;</span>);<br>    &#125;<br>&#125;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title">Program</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Main</span>()</span><br>    &#123;<br>        ILogger logger = <span class="hljs-keyword">new</span> ConsoleLogger();<br>        <br>        <span class="hljs-comment">// 调用普通接口方法</span><br>        logger.Log(<span class="hljs-string">&quot;这是一个日志消息&quot;</span>);<br><br>        <span class="hljs-comment">// 调用默认接口方法</span><br>        <span class="hljs-comment">// 注意：默认方法只能通过接口变量调用，不能通过类变量调用</span><br>        logger.LogError(<span class="hljs-string">&quot;这是一个错误消息&quot;</span>);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>这样，类库作者可以在不破坏兼容性的前提下向接口添加新功能，而实现类也可以选择是否覆盖默认实现来提供更具体的行为。但是我们依然应当明确接口和抽象类的设计意图：</p><ul><li><strong>接口</strong> 主要用于定义一个契约，强调的是<strong>能力</strong> (can-do)，它告诉我们一个类能做什么，但不关心它是如何实现的。</li><li><strong>抽象类</strong> 则更强调<strong>身份</strong> (is-a)，它告诉我们一个类是什么，同时也提供了一些默认的实现，供派生类继承和扩展。</li></ul><h4 id="密封类"><a class="markdownIt-Anchor" href="#密封类"></a> 密封类</h4><p>当然，不是所有类都适合被继承的，如果一个类不希望被继承，可以使用 <code>sealed</code> 关键字来修饰这个类，表示这个类不能被其他类继承。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">sealed</span> <span class="hljs-keyword">class</span> <span class="hljs-title">FinalClass</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">DoSomething</span>()</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;This class cannot be inherited.&quot;</span>);<br>    &#125;<br>&#125;<br><br><span class="hljs-comment">// 报错 CS0509 &#x27;DerivedClass&#x27;: cannot derive from sealed type &#x27;FinalClass&#x27;</span><br><span class="hljs-keyword">class</span> <span class="hljs-title">DerivedClass</span> : <span class="hljs-title">FinalClass</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">DoSomethingElse</span>()</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;This will never compile.&quot;</span>);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><h4 id="组合优于继承"><a class="markdownIt-Anchor" href="#组合优于继承"></a> 组合优于继承</h4><p>在过去的工程实践中，过度使用继承导致了代码的僵化和难以维护的问题。随着软件设计原则的发展，<strong>组合优于继承</strong> <em>(Composition over Inheritance)</em> 的理念逐渐被广泛接受。</p><p>深层继承像多米诺骨牌，一个类的改变可能会引发整个继承链的连锁反应，导致大量的代码需要修改和测试。而现在的业务需求很少会完美契合严格的树状 is-a 关系。</p><details class="tag-plugin colorful folding" color="gray" open><summary><p>依然适合继承</p></summary><div class="body"><p>在设计 UI 组件库时，继承仍然是一个非常合适的选择。比如我们有一个 <code>Button</code> 类，继承自 <code>Control</code> 类，这样我们就可以在 <code>Button</code> 类中重用 <code>Control</code> 类的属性和方法，同时还可以添加一些特定于按钮的功能。同样，UI 组件库的扩展性也非常强，新的组件往往会在现有组件的基础上进行扩展，这时候继承就显得非常自然和高效了。</p> </div></details><p><strong>组合</strong> <em>(Composition)</em> 则是将关系从 is-a 转变为 has-a，通过将一个类的实例作为另一个类的成员来实现功能的复用和扩展。这种方式更灵活，可以在运行时动态地改变对象的行为，而不需要修改类的定义。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">Engine</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Start</span>()</span> =&gt; Console.WriteLine(<span class="hljs-string">&quot;Engine started.&quot;</span>);<br>&#125;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title">Car</span><br>&#123;<br>    <span class="hljs-keyword">private</span> Engine _engine = <span class="hljs-keyword">new</span> Engine(); <span class="hljs-comment">// 组合关系</span><br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Start</span>()</span><br>    &#123;<br>        _engine.Start(); <span class="hljs-comment">// 使用组合的对象来实现功能</span><br>        Console.WriteLine(<span class="hljs-string">&quot;Car started.&quot;</span>);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><h5 id="委托模式"><a class="markdownIt-Anchor" href="#委托模式"></a> 委托模式</h5><p>跳出 C#，隔壁 Kotlin 语言借助<strong>委托模式</strong> <em>(Delegation Pattern)</em> 来解决了继承带来的问题。它允许组合实现与继承相同的代码复用。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs kotlin"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Rectangle</span>(<span class="hljs-keyword">val</span> width: <span class="hljs-built_in">Int</span>, <span class="hljs-keyword">val</span> height: <span class="hljs-built_in">Int</span>) &#123;<br>    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">area</span><span class="hljs-params">()</span></span> = width * height<br>&#125;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">Window</span>(<span class="hljs-keyword">val</span> bounds: Rectangle) &#123;<br>    <span class="hljs-comment">// 通过委托来复用 Rectangle 的 area 方法</span><br>    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">area</span><span class="hljs-params">()</span></span> = bounds.area()<br>&#125;<br></code></pre></td></tr></table></figure><p>Kotlin 的 <code>by</code> 关键字可以将操作委托给另一个对象的接口。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs kotlin"><span class="hljs-keyword">interface</span> <span class="hljs-title class_">ClosedShape</span> &#123;<br>    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">area</span><span class="hljs-params">()</span></span>: <span class="hljs-built_in">Int</span><br>&#125;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">Rectangle</span>(<span class="hljs-keyword">val</span> width: <span class="hljs-built_in">Int</span>, <span class="hljs-keyword">val</span> height: <span class="hljs-built_in">Int</span>) : ClosedShape &#123;<br>    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">area</span><span class="hljs-params">()</span></span> = width * height<br>&#125;<br><br><span class="hljs-comment">// Window 类通过委托实现 ClosedShape 接口，直接使用 Rectangle 的 area 方法</span><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">Window</span>(<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> bounds: Rectangle) : ClosedShape <span class="hljs-keyword">by</span> bounds<br></code></pre></td></tr></table></figure><h3 id="多态"><a class="markdownIt-Anchor" href="#多态"></a> 多态</h3><p>不同于另外两个特性，<strong>多态</strong> <em>(Polymorphism)</em> 是一个更为抽象的概念，它代表着以不同的形式表现出来的能力。</p><div class="tag-plugin image"><div class="image-bg" style="width:100%;"><img class="lazy" src="/images/2026/dotnet-oop-encapsulation-inheritance-polymorphism/polymorph.webp" data-src="/images/2026/dotnet-oop-encapsulation-inheritance-polymorphism/polymorph.webp" alt="Polymorph" data-fancybox="true" style="width:400px;"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">Polymorph</span></div></div><p>太抽象了，但是实际的意义其实非常非常简单：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">Shape</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Draw</span>()</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;Drawing a shape.&quot;</span>);<br>    &#125;<br>&#125;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title">Circle</span> : <span class="hljs-title">Shape</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Draw</span>()</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;Drawing a circle.&quot;</span>);<br>    &#125;<br>&#125;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title">Square</span> : <span class="hljs-title">Shape</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Draw</span>()</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;Drawing a square.&quot;</span>);<br>    &#125;<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">DrawShape</span>(<span class="hljs-params">Shape shape</span>)</span><br>&#123;<br>    shape.Draw(); <span class="hljs-comment">// 多态调用，根据实际类型调用对应的 Draw 方法</span><br>&#125;<br></code></pre></td></tr></table></figure><p>在这个例子中，<code>DrawShape</code> 方法接受一个 <code>Shape</code> 类型的参数，但它可以接受任何继承自 <code>Shape</code> 的对象（如 <code>Circle</code> 或 <code>Square</code>）。当我们调用 <code>shape.Draw()</code> 时，实际调用的是对象的运行时类型对应的 <code>Draw</code> 方法，无需编写<code>if-else</code> 或 <code>switch</code> 语句来判断对象的类型，这就是多态。</p><h4 id="混淆"><a class="markdownIt-Anchor" href="#混淆"></a> 混淆</h4><p>既然都是重写，那么它和<code>abstract</code>方法的区别是什么呢？抽象方法没有任何实现，派生类<strong>必须</strong>提供一个实现来覆盖它；而虚方法则有一个默认的实现，派生类<strong>可以选择</strong>是否覆盖它来提供更具体的行为。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">Base</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Method</span>()</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;Base implementation.&quot;</span>);<br>    &#125;<br>&#125;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title">Derived</span> : <span class="hljs-title">Base</span><br>&#123;<br>    <span class="hljs-comment">// 重写基类的虚方法，提供新的实现，但不是必须的</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Method</span>()</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;Derived implementation.&quot;</span>);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><hr /><p>在定义方法的时候，开发者可以和实例化一个对象一样，<code>new</code>一个方法来隐藏基类的方法，这被称为<strong>方法隐藏</strong> <em>(Method Hiding)</em>，它和重写是不同的概念。方法隐藏使用 <code>new</code> 关键字来声明，表示这个方法将隐藏基类中同名的方法，而不是重写它。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">Base</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Method</span>()</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;Base implementation.&quot;</span>);<br>    &#125;<br>&#125;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title">Derived</span> : <span class="hljs-title">Base</span><br>&#123;<br>    <span class="hljs-comment">// 使用 new 关键字隐藏基类的方法，而不是重写它</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">new</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Method</span>()</span><br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;Derived implementation.&quot;</span>);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>和<code>override</code>相对，通过<code>new</code>来隐藏方法会使其不再多态，所以会出现以下差异。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs csharp">Base baseObj = <span class="hljs-keyword">new</span> Derived();<br><br><span class="hljs-comment">// 如果是 override，那么调用的是 Derived 的 Method 方法，输出 &quot;Derived implementation.&quot;</span><br><span class="hljs-comment">// 如果是 new，那么调用的是 Base 的 Method 方法，输出 &quot;Base implementation.&quot;</span><br>baseObj.Method();<br></code></pre></td></tr></table></figure><p>在实际使用中，隐藏方法其实并不常见，仅作为诸如防御性的版本控制等特殊场景下的一个工具而存在。大多数情况下，我们更倾向于使用重写来实现多态，因为它更符合面向对象设计的原则。</p><h4 id="实现机制"><a class="markdownIt-Anchor" href="#实现机制"></a> 实现机制</h4><p>多态的实现机制主要依赖于<strong>虚方法表</strong> <em>(Virtual Method Table, VTable)</em>，当一个方法被标记为 <code>virtual</code> 时，编译器会在类的内部创建一个虚方法表来存储该方法的地址。</p><p>当派生类重写这个方法时，虚方法表会更新为指向新的方法实现，这样在<strong>运行时</strong>就能够根据对象的实际类型来调用正确的方法了。</p><p>为什么在运行时进行而不在编译时进行呢？因为在编译时，编译器只能知道变量的静态类型（如 <code>Shape</code>），而无法确定它在运行时会引用哪个具体的对象（如 <code>Circle</code> 或 <code>Square</code>）。</p><p>因此，只有在运行时才能根据对象的实际类型来决定调用哪个方法实现，这个过程也被称为<strong>动态绑定</strong> <em>(Dynamic Binding)</em> 或 <strong>后期绑定</strong> <em>(Late Binding)</em>。</p><p>一个对象的虚方法表会包含其动态绑定方法的<strong>地址</strong>，同一类的所有对象共享同一个虚方法表，而类型兼容的类（如同一个基类的派生类们）则会共享相同的虚方法表<strong>结构</strong>。</p><p>让我们从 C# 层开始：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Animal</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Speak</span>()</span> <br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;Animal speaks&quot;</span>);<br>    &#125;<br>&#125;<br><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Dog</span> : <span class="hljs-title">Animal</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Speak</span>()</span> <br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;Dog barks&quot;</span>);<br>    &#125;<br>&#125;<br><br>Animal myAnimal = <span class="hljs-keyword">new</span> Dog();<br>myAnimal.Speak(); <span class="hljs-comment">// 输出: &quot;Dog barks&quot;</span><br></code></pre></td></tr></table></figure><p>透过 IL 代码：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs il">IL_0001newobjDog..ctor<br>IL_0006stloc.0   // myAnimal<br>IL_0007ldloc.0   // myAnimal<br>IL_0008callvirtAnimal.Speak ()<br></code></pre></td></tr></table></figure><p>这里的关键点在于<code>callvirt</code>指令，和<code>call</code>的静态绑定不同，它会对对象调用后期绑定方法，在这里根据 <code>myAnimal</code> 的实际类型来调用正确的 <code>Speak</code> 方法实现。</p><p><em>有关<code>callvirt</code>指令的更多细节，可以参考 <a href="https://learn.microsoft.com/zh-cn/dotnet/api/system.reflection.emit.opcodes.callvirt?view=net-10.0">Microsoft Learn - callvirt</a>。</em></p><p>.NET 的 CLR 通过维护一套底层数据结构来支持这个操作，当一个对象被实例化并在堆上分配内存时，它的内存布局如下：</p><ul><li>对象头（Object Header）：包含一些运行时信息，如类型指针、同步块索引等。</li><li>方法表指针（Method Table Pointer）：指向一个方法表，这个方法表包含了该对象所属类型的所有方法的地址。</li><li>实例字段（Instance Fields）：存储对象的实际数据成员。</li></ul><p>这里存储的不是方法表，而是一个指向方法表的指针，因为每个被 CLR 加载的类型都只有一个方法表，而所有该类型的对象都共享这个方法表。通过这个指针，CLR 就能够在运行时根据对象的实际类型来查找并调用正确的方法实现了。</p><p>而每个方法表在内存中都为虚方法留有槽位，每个槽位存储着该类某个虚方法在内存中实际的入口地址。</p><p>总之，这句 IL 在将这段 IL 转换为机器码时：</p><ol><li>拿到<code>myAnimal</code>指向堆内存的起始地址，获取对象引用</li><li>通过对象的起始地址偏移获取方法表指针</li><li>由于<code>Animal.Speak()</code>在编译期确定了虚方法表中的某个固定槽位</li><li>通过方法表指针加上这个槽位的偏移来获取实际方法的地址（在这里是 <code>Dog.Speak()</code> 的地址）</li><li>调用这个地址来执行方法</li></ol><p>由于虚方法在 C# 是需要被显式声明的，所以对于非虚方法，CPU 会 direct call 这个方法的地址，而不是通过虚方法表来调用。这就意味着工程师需要对虚方法的调用过程做优化，这被称为<strong>去虚化</strong> <em>(Devirtualization)</em>，它是编译器优化的一种技术，通过分析代码来确定某些虚方法调用实际上可以被静态绑定，从而直接调用方法的地址来提高性能。</p><h2 id="案例支付系统"><a class="markdownIt-Anchor" href="#案例支付系统"></a> 案例：支付系统</h2><p>在一个支付系统中，存在一个接口<code>IPaymentProcessor</code>，该接口规定了所有支付网关都必须实现的功能，如<code>ProcessPayment</code>方法。不同的支付网关（如 PayPal、Stripe、Square 等）都实现了这个接口，但它们的具体实现细节可能完全不同。这个设计过程借助了接口而非实现的思路，实现了依赖倒置，与第三方支付网关的耦合度大大降低了。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IPaymentProcessor</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-built_in">bool</span> <span class="hljs-title">ProcessPayment</span>(<span class="hljs-params"><span class="hljs-built_in">decimal</span> amount</span>)</span>;<br>&#125;<br><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">PayPalProcessor</span> : <span class="hljs-title">IPaymentProcessor</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">ProcessPayment</span>(<span class="hljs-params"><span class="hljs-built_in">decimal</span> amount</span>)</span><br>    &#123;<br>        <span class="hljs-comment">// PayPal-specific implementation</span><br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;<br>    &#125;<br>&#125;<br><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">StripeProcessor</span> : <span class="hljs-title">IPaymentProcessor</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">ProcessPayment</span>(<span class="hljs-params"><span class="hljs-built_in">decimal</span> amount</span>)</span><br>    &#123;<br>        <span class="hljs-comment">// Stripe-specific implementation</span><br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>为了处理这些支付业务中的共性逻辑，比如网络重试、全局日志等，我们可以引入一个抽象类<code>BasePaymentGateway</code>，它实现了<code>IPaymentProcessor</code>接口，利用<code>protected</code>访问修饰符来封装了各种 HTTP 请求的辅助方法。这种设计使得底层的基建代码仅对具体的网关子类可见，而不会暴露给外部使用者，从而实现了封装。</p><p>在基类中，<code>ProcessPayment</code>方法被定义为抽象方法，作为模板，处理完日志后，调用<code>ExecuteTransactionCore</code>方法来执行具体的支付逻辑，而这个方法则由派生类来实现。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">class</span> <span class="hljs-title">BasePaymentGateway</span> : <span class="hljs-title">IPaymentProcessor</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">ProcessPayment</span>(<span class="hljs-params"><span class="hljs-built_in">decimal</span> amount</span>)</span><br>    &#123;<br>        <span class="hljs-comment">// 处理共性逻辑，如日志、重试等</span><br>        LogPaymentAttempt(amount);<br>        <br>        <span class="hljs-comment">// 调用抽象方法执行具体的支付逻辑</span><br>        <span class="hljs-keyword">return</span> ExecuteTransactionCore(amount);<br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">abstract</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">ExecuteTransactionCore</span>(<span class="hljs-params"><span class="hljs-built_in">decimal</span> amount</span>)</span>;<br><br>    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">LogPaymentAttempt</span>(<span class="hljs-params"><span class="hljs-built_in">decimal</span> amount</span>)</span><br>    &#123;<br>        <span class="hljs-comment">// 记录支付尝试的日志</span><br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>而派生类必须实现<code>ExecuteTransactionCore</code>方法来提供具体的支付逻辑，这样就实现了多态，调用者只需要通过接口来调用<code>ProcessPayment</code>方法，而不需要关心具体的支付网关实现细节。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">PayPalGateway</span> : <span class="hljs-title">BasePaymentGateway</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">override</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">ExecuteTransactionCore</span>(<span class="hljs-params"><span class="hljs-built_in">decimal</span> amount</span>)</span><br>    &#123;<br>        <span class="hljs-comment">// PayPal-specific transaction logic</span><br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;<br>    &#125;<br>&#125;<br><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">StripeGateway</span> : <span class="hljs-title">BasePaymentGateway</span><br>&#123;<br>    <span class="hljs-comment">// 同时借助修饰符来封装了 Stripe 的 API 密钥等敏感信息，使得它们只能在 StripeGateway 类内部访问。</span><br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> <span class="hljs-built_in">string</span> _apiKey;<br><br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">override</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">ExecuteTransactionCore</span>(<span class="hljs-params"><span class="hljs-built_in">decimal</span> amount</span>)</span><br>    &#123;<br>        <span class="hljs-comment">// Stripe-specific transaction logic</span><br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>在最后，我们可以利用<strong>依赖注入</strong> <em>(Dependency Injection)</em> 容器，仅持有一个简单的<code>IPaymentProcessor</code>接口的引用来处理支付业务，而不需要关心具体的支付网关实现，这样就实现了面向接口编程，降低了系统的耦合度，提高了代码的可维护性和扩展性。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">PaymentService</span><br>&#123;<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IPaymentProcessor _paymentProcessor;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">PaymentService</span>(<span class="hljs-params">IPaymentProcessor paymentProcessor</span>)</span><br>    &#123;<br>        _paymentProcessor = paymentProcessor;<br>    &#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">MakePayment</span>(<span class="hljs-params"><span class="hljs-built_in">decimal</span> amount</span>)</span><br>    &#123;<br>        <span class="hljs-keyword">if</span> (_paymentProcessor.ProcessPayment(amount))<br>        &#123;<br>            Console.WriteLine(<span class="hljs-string">&quot;Payment successful.&quot;</span>);<br>        &#125;<br>        <span class="hljs-keyword">else</span><br>        &#123;<br>            Console.WriteLine(<span class="hljs-string">&quot;Payment failed.&quot;</span>);<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h2><p>.NET 在发展过程中不断引入新的特性来丰富 OOP 的表达能力，如记录、接口默认方法、仅初始化属性等，使得开发者能够更灵活地设计和实现面向对象的系统。开发者们借助封装建立起系统的边界，利用继承来构建概念分类，并通过多态来实现行为抽象，从而构建出高内聚、低耦合、易维护的代码结构。</p>]]>
    </content>
    <id>https://ziling.moe/2026/dotnet-oop-encapsulation-inheritance-polymorphism/</id>
    <link href="https://ziling.moe/2026/dotnet-oop-encapsulation-inheritance-polymorphism/"/>
    <published>2026-03-12T06:10:00.000Z</published>
    <summary>面向对象编程（OOP）是软件开发中的一种重要范式，强调通过封装、继承和多态来组织代码和数据。本文将深入探讨这三个核心概念，并通过示例来说明它们在 .NET 中的应用。</summary>
    <title>.NET 面向对象 - 封装、继承与多态</title>
    <updated>2026-03-12T06:10:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term=".NET Learning" scheme="https://ziling.moe/categories/NET-Learning/"/>
    <category term=".NET" scheme="https://ziling.moe/tags/NET/"/>
    <category term="数据结构" scheme="https://ziling.moe/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    <content>
      <![CDATA[<p>在工程场景中，我们经常会借助<strong>线性表</strong> <em>(Linear List)</em> 的机制来组织和管理数据，尤其是面对顺序存取、数据缓冲、流式处理等场景时，它们是构建程序逻辑最基础、最核心的部分。</p><p>在 .NET 的历史中，线性表数据结构也经历了从可用到高性能的多次演进。从早期的非泛型集合到现代的内存切片技术，其背后的实现原理虽然遵循着经典的计算机科学理论，但为了应对不断严苛的内存分配优化（GC 压力）和现代 CPU 的缓存友好性，.NET 在底层实现上进行了大量精妙的改进。</p><p>本文选用的主线是动态数组，比如<code>ArrayList</code>、<code>List&lt;T&gt;</code>、<code>ConcurrentBag&lt;T&gt;</code>等，来展示线性表在 .NET 中的演进历程。虽然还有链表、队列、栈等其他线性表类型，但动态数组的演进更能体现 .NET 在性能优化和内存管理上的创新。</p><p><em>同时，本文寻找的源代码均为最新，比如曾经的<code>object</code>可能变成了<code>object?</code>，但这并不影响我们对其设计和实现的理解。</em></p><h2 id="arraylist"><a class="markdownIt-Anchor" href="#arraylist"></a> ArrayList</h2><p><code>System.Collections.ArrayList</code> 是早期 .NET 1.0 提供的非泛型动态数组实现，用于替代固定长度数组，其内部使用<code>object[]</code>来存储元素。</p><h3 id="细节"><a class="markdownIt-Anchor" href="#细节"></a> 细节</h3><blockquote><p><code>ArrayList</code> 不支持使用多维数组（如<code>int[,,]</code>）作为元素。</p></blockquote><p>这一点是微软文档提到的，但在实际测试中，<code>ArrayList</code> 是可以存储多维数组的，因为它存储的是 <code>object</code> 类型的引用，所以理论上可以存储任何类型的对象，包括多维数组。</p><p>个人认为在实际使用中，官方的态度是不建议将多维数组作为 <code>ArrayList</code> 的元素，因为导致的代码可读性和维护性问题，比如：</p><ul><li>微软更推荐使用交错数组（如<code>int[][]</code>）来代替多维数组，这样更直观且规范。</li><li>在微软的<a href="https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/parameter-design">设计指南</a>中提到：不要公开接受指针、指针数组或多维数组作为参数的方法。因为指针和多维数组的使用相对复杂。几乎在所有情况下，都可以避免在 API 中使用它们。</li></ul><blockquote><p><code>ArrayList</code> 有 <code>IsSynchronized</code> 属性，指对 <code>ArrayList</code> 的访问是否是同步的（线程安全的）。</p></blockquote><p>这个属性实际上是来自其实现的<code>IList -&gt; ICollection</code>接口（不是带泛型的<code>IList&lt;T&gt; -&gt; ICollection&lt;T&gt;</code>）。同样，它里面也有个<code>SyncRoot</code>属性，提供了一个对象来同步访问<code>ArrayList</code>，开发者需要自己使用<code>lock</code>语句来确保线程安全。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs csharp">ICollection col = <span class="hljs-keyword">new</span> ArrayList();<br><span class="hljs-keyword">lock</span> (col.SyncRoot) &#123;<br>    <span class="hljs-comment">// 线程安全的访问 ArrayList</span><br>&#125;<br></code></pre></td></tr></table></figure><blockquote><p>在 .NET Framework 平台，通过指定<code>&lt;gcAllowVeryLargeObjects&gt;</code>的<code>enabled</code>设置为<code>true</code>，可以允许创建超过 2 GB 的数组。</p></blockquote><p>在<code>App.config</code>文件中添加以下配置即可：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">configuration</span>&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">runtime</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">gcAllowVeryLargeObjects</span> <span class="hljs-attr">enabled</span>=<span class="hljs-string">&quot;true&quot;</span> /&gt;</span><br>  <span class="hljs-tag">&lt;/<span class="hljs-name">runtime</span>&gt;</span><br><span class="hljs-tag">&lt;/<span class="hljs-name">configuration</span>&gt;</span><br></code></pre></td></tr></table></figure><hr /><p>对比于之后推出的<code>IList&lt;T&gt;</code>，<code>ArrayList</code> 的设计虽然简单，而且支持存储异构对象集合，它存在以下显著的缺点。</p><h3 id="类型安全问题"><a class="markdownIt-Anchor" href="#类型安全问题"></a> 类型安全问题</h3><p>找到<code>ArrayList</code><a href="https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Collections/ArrayList.cs,153">源代码</a>的<code>Add</code>方法，会发现它接受的参数类型为 <code>object</code>。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> <span class="hljs-built_in">int</span> <span class="hljs-title">Add</span>(<span class="hljs-params"><span class="hljs-built_in">object</span>? <span class="hljs-keyword">value</span></span>)</span><br>&#123;<br>    <span class="hljs-keyword">if</span> (_size == _items.Length) EnsureCapacity(_size + <span class="hljs-number">1</span>);<br>    _items[_size] = <span class="hljs-keyword">value</span>;<br>    _version++;<br>    <span class="hljs-keyword">return</span> _size++;<br>&#125;<br></code></pre></td></tr></table></figure><p>因为你可以将任何类型的对象放入同一个<code>ArrayList</code>中，这就导致在尝试取出其数据时，必须进行<strong>显式类型转换</strong>，如果转换失败，就会抛出运行时错误。</p><p>所以<code>ArrayList</code>无法保证编译时类型安全，容易引发运行时错误，但这并不是它本身的问题，因为泛型是在 C# 2.0 引入的，这个问题在当时确实无法避免。</p><details class="tag-plugin colorful folding" color="orange" open><summary><p>不建议使用</p></summary><div class="body"><p>现如今，新项目建议直接使用<code>List&lt;T&gt;</code>，详见<a href="https://github.com/dotnet/platform-compat/blob/master/docs/DE0006.md">DE0006: Non-generic collections shouldn’t be used</a></p> <ul> <li>对于异构对象集合，请使用 <code>List&lt;Object&gt;</code></li> <li>对于同构对象集合，请使用 <code>List&lt;T&gt;</code></li> </ul> </div></details><h3 id="装箱与拆箱的性能问题"><a class="markdownIt-Anchor" href="#装箱与拆箱的性能问题"></a> 装箱与拆箱的性能问题</h3><p>上述的类型安全问题同时引入了<code>object</code>类型的装箱和拆箱操作，尤其是对于值类型（如<code>int</code>、<code>double</code>等）来说，这些操作会导致性能下降和 GC 压力增加。</p><p>尽管在如今的 .NET 版本中，JIT 编译器已经对装箱和拆箱进行了优化，但在当时的环境中，这确实是一个显著的性能问题（现在同样也强烈建议避免装箱和拆箱操作）。</p><h3 id="可读性和维护性问题"><a class="markdownIt-Anchor" href="#可读性和维护性问题"></a> 可读性和维护性问题</h3><p>同上，缺乏泛型约束的<code>ArrayList</code>使得代码的意图不够明确。对比<code>List&lt;T&gt;</code>，它的类型参数明确了元素的类型，而<code>ArrayList</code>则需要开发者在使用前确认其内容的类型，且在使用过程中也需要频繁进行类型检查和转换。</p><p>如果你尝试在<code>ArrayList</code>访问一个不存在的索引，返回的结果是<code>null</code>，虽然谈不上是缺点，但是需要在约定上总是检查返回值是否为<code>null</code>，否则可能会引发<code>NullReferenceException</code>。</p><h2 id="list"><a class="markdownIt-Anchor" href="#list"></a> List</h2><p>在引入了泛型之后，<code>System.Collections.Generic.List&lt;T&gt;</code>使用了泛型数组<code>T[]</code>作为底层容器，成为了<code>ArrayList</code>的更现代化替代品，提供了类型安全、性能优化和更丰富的功能。和<code>ArrayList</code>一样，<code>List&lt;T&gt;</code>每次扩容通常按倍数增加容量，但由于元素类型确定，避免了装箱和拆箱的性能问题。</p><h3 id="细节-2"><a class="markdownIt-Anchor" href="#细节-2"></a> 细节</h3><blockquote><p>为了避免<code>Array.Empty&lt;T&gt;()</code>的额外泛型实例化，<code>List&lt;T&gt;</code>在内部维护了一个静态只读字段<code>s_emptyArray</code>，指向一个空数组。</p></blockquote><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">readonly</span> T[] s_emptyArray = <span class="hljs-keyword">new</span> T[<span class="hljs-number">0</span>];<br></code></pre></td></tr></table></figure><blockquote><p>神奇的<code>_version</code>字段</p></blockquote><p><code>List&lt;T&gt;</code>内部维护了一个<code>_version</code>字段，每当对列表进行修改（如添加、删除元素）时，都会增加这个版本号。</p><p>这个设计主要是为了支持迭代器的异常安全 <em>(fail-fast)</em> 机制。当你使用<code>foreach</code>或显式创建一个迭代器时，迭代器会捕获当前的版本号，如果在迭代过程中检测到版本号发生了变化（即列表被修改了），就会抛出<code>InvalidOperationException</code>，以防止迭代器在不安全的状态下继续操作。比如<code>ForEach</code>的实现：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">ForEach</span>(<span class="hljs-params">Action&lt;T&gt; action</span>)</span><br>&#123;<br>    <span class="hljs-keyword">if</span> (action == <span class="hljs-literal">null</span>)<br>    &#123;<br>        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.action);<br>    &#125;<br><br>    <span class="hljs-built_in">int</span> version = _version;<br><br>    <span class="hljs-keyword">for</span> (<span class="hljs-built_in">int</span> i = <span class="hljs-number">0</span>; i &lt; _size; i++)<br>    &#123;<br>        <span class="hljs-keyword">if</span> (version != _version)<br>        &#123;<br>            <span class="hljs-keyword">break</span>;<br>        &#125;<br>        action(_items[i]);<br>    &#125;<br><br>    <span class="hljs-keyword">if</span> (version != _version)<br>        ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();<br>&#125;<br></code></pre></td></tr></table></figure><blockquote><p><code>Enumerator</code>是结构体而不是类</p></blockquote><p><code>List&lt;T&gt;.Enumerator</code>是一个结构体（<code>struct</code>），而不是一个类（<code>class</code>）。结构体作为值类型，直接在栈上分配内存，而类作为引用类型，需要在堆上分配内存，并且需要垃圾回收来管理生命周期。尤其是在频繁迭代的场景中，使用结构体可以减少内存分配和垃圾回收的开销。</p><blockquote><p><code>Clear()</code>的优化小巧思</p></blockquote><p>观察<code>List&lt;T&gt;.Clear()</code>方法的实现：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs csharp">[<span class="hljs-meta">MethodImpl(MethodImplOptions.AggressiveInlining)</span>]<br><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Clear</span>()</span><br>&#123;<br>    _version++;<br>    <span class="hljs-keyword">if</span> (RuntimeHelpers.IsReferenceOrContainsReferences&lt;T&gt;())<br>    &#123;<br>        <span class="hljs-built_in">int</span> size = _size;<br>        _size = <span class="hljs-number">0</span>;<br>        <span class="hljs-keyword">if</span> (size &gt; <span class="hljs-number">0</span>)<br>        &#123;<br>            Array.Clear(_items, <span class="hljs-number">0</span>, size); <span class="hljs-comment">// Clear the elements so that the gc can reclaim the references.</span><br>        &#125;<br>    &#125;<br>    <span class="hljs-keyword">else</span><br>    &#123;<br>        _size = <span class="hljs-number">0</span>;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>我们会发现它针对值类型和引用类型做了不同的处理。对于值类型，直接将<code>_size</code>设置为0即可，因为值类型不涉及垃圾回收；而对于引用类型，则需要调用<code>Array.Clear</code>来清除数组中的元素引用，以便垃圾回收器能够回收这些对象。这种优化避免了对值类型进行不必要的清理操作，提高了性能。</p><h3 id="并发问题"><a class="markdownIt-Anchor" href="#并发问题"></a> 并发问题</h3><p>虽然<code>List&lt;T&gt;</code>表现出色，但它并不是线程安全的。</p><p>文档提到，它的<code>public static</code>成员都是线程安全的，但实例成员则不是。这意味着多个线程可以安全地访问<code>List&lt;T&gt;</code>的静态成员（如<code>List&lt;T&gt;.Empty</code>），但如果多个线程同时访问同一个<code>List&lt;T&gt;</code>实例进行读写操作，就可能会导致数据竞争和不确定的行为。</p><p>严格来说，如果没有线程在写，多线程读取是安全的。如果需要在多线程环境中使用<code>List&lt;T&gt;</code>，可以考虑使用<code>ConcurrentBag&lt;T&gt;</code>、<code>ImmutableList&lt;T&gt;</code>集合类型，或者在访问<code>List&lt;T&gt;</code>时使用锁来确保线程安全。</p><h2 id="readonlycollection"><a class="markdownIt-Anchor" href="#readonlycollection"></a> ReadOnlyCollection</h2><p>在设计 API 的时候，如果需要将可变集合以只读的形式公开，防止外部修改，可以使用<code>System.Collections.ObjectModel.ReadOnlyCollection&lt;T&gt;</code>。它是一个包装类，内部持有一个<code>IList&lt;T&gt;</code>的引用，并通过实现<code>IReadOnlyList&lt;T&gt;</code>接口来提供只读访问。但它不是真正的不可变，例如：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">var</span> modifiableList = <span class="hljs-keyword">new</span> List&lt;<span class="hljs-built_in">int</span>&gt; &#123; <span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span> &#125;;<br><span class="hljs-keyword">var</span> readOnlyCollection = <span class="hljs-keyword">new</span> ReadOnlyCollection&lt;<span class="hljs-built_in">int</span>&gt;(modifiableList);<br>Console.WriteLine(<span class="hljs-built_in">string</span>.Join(<span class="hljs-string">&quot;, &quot;</span>, readOnlyCollection)); <span class="hljs-comment">// 输出: 1, 2, 3</span><br>modifiableList.Add(<span class="hljs-number">4</span>);<br>Console.WriteLine(<span class="hljs-built_in">string</span>.Join(<span class="hljs-string">&quot;, &quot;</span>, readOnlyCollection)); <span class="hljs-comment">// 输出: 1, 2, 3, 4</span><br>readOnlyCollection[<span class="hljs-number">0</span>] = <span class="hljs-number">10</span>; <span class="hljs-comment">// 编译错误，无法修改 ReadOnlyCollection 的元素</span><br></code></pre></td></tr></table></figure><p>能看出来的是，它更适合用于规范 API 的设计，为了方便实现者创建通用的只读自定义集合。鼓励实现者扩展此基类，而不是创建自己的基类。</p><h3 id="只是皮套"><a class="markdownIt-Anchor" href="#只是皮套"></a> 只是皮套</h3><p><code>ReadOnlyCollection&lt;T&gt;</code> 只是一个包装类，它并没有自己的数据存储，而是持有一个对原始集合的引用。当你创建一个<code>ReadOnlyCollection&lt;T&gt;</code>实例时，你需要传入一个实现了<code>IList&lt;T&gt;</code>接口的集合（如<code>List&lt;T&gt;</code>）。这个包装类通过实现<code>IReadOnlyList&lt;T&gt;</code>接口来提供只读访问，但它并不复制原始集合的数据，因此如果原始集合发生变化，<code>ReadOnlyCollection&lt;T&gt;</code>也会反映这些变化，如上面例子所示。同样，观察其初始化代码可知：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IList&lt;T&gt; list;<br> <br><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">ReadOnlyCollection</span>(<span class="hljs-params">IList&lt;T&gt; list</span>)</span><br>&#123;<br>    <span class="hljs-keyword">if</span> (list == <span class="hljs-literal">null</span>)<br>    &#123;<br>        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.list);<br>    &#125;<br>    <span class="hljs-keyword">this</span>.list = list;<br>&#125;<br></code></pre></td></tr></table></figure><p>所以，它的性能开销非常小，因为它没有进行数据复制，只是持有一个引用。</p><h3 id="显式接口实现"><a class="markdownIt-Anchor" href="#显式接口实现"></a> 显式接口实现</h3><p>观察它的<code>Add</code>方法：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">void</span> ICollection&lt;T&gt;.Add(T <span class="hljs-keyword">value</span>)<br>&#123;<br>    ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_ReadOnlyCollection);<br>&#125;<br></code></pre></td></tr></table></figure><p>不同于直接实现，<code>ReadOnlyCollection&lt;T&gt;</code>通过<strong>显式接口实现</strong> <em>(Explicit Interface Implementation)</em> 来隐藏修改方法（如<code>Add</code>、<code>Remove</code>等）。</p><div class="tag-plugin image"><div class="image-bg" style="width:100%;"><img class="lazy" src="/images/2026/dotnet-data-structure-linear-list/add-not-found.webp" data-src="/images/2026/dotnet-data-structure-linear-list/add-not-found.webp" alt="找不到方法" data-fancybox="true" style="width:600px;"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">找不到方法</span></div></div><p>当你尝试调用<code>readOnlyCollection.Add(5)</code>时，编译器会提示找不到<code>Add</code>方法，因为它是通过显式接口实现的，只有当你将<code>ReadOnlyCollection&lt;T&gt;</code>实例转换为<code>ICollection&lt;T&gt;</code>类型时，才会暴露这些方法，但此时调用它们会抛出<code>NotSupportedException</code>异常。这种设计使得<code>ReadOnlyCollection&lt;T&gt;</code>在编译时就无法被误用来修改集合，从而增强了代码的安全性和可读性。</p><div class="tag-plugin image"><div class="image-bg" style="width:100%;"><img class="lazy" src="/images/2026/dotnet-data-structure-linear-list/add-not-supported.webp" data-src="/images/2026/dotnet-data-structure-linear-list/add-not-supported.webp" alt="不支持修改" data-fancybox="true" style="width:600px;"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">不支持修改</span></div></div><h2 id="concurrentbag"><a class="markdownIt-Anchor" href="#concurrentbag"></a> ConcurrentBag</h2><p>说回<code>List&lt;T&gt;</code>的线程安全问题，.NET 提供了<code>System.Collections.Concurrent</code>命名空间，其中的<code>ConcurrentBag&lt;T&gt;</code>是一个线程安全的无序集合，适用于生产者-消费者场景。它内部使用了分段锁和线程局部存储来实现高效的并发访问，允许多个线程同时添加和移除元素，而不会引起数据竞争或不确定的行为。</p><p><em>竟然真的有继承<code>IProducerConsumerCollection&lt;T&gt;</code>这个名字的接口？</em></p><h3 id="线程局部存储"><a class="markdownIt-Anchor" href="#线程局部存储"></a> 线程局部存储</h3><p>从<code>Add</code>和<code>TryTake</code>方法来看：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Add</span>(<span class="hljs-params">T item</span>)</span> =&gt;<br>    GetCurrentThreadWorkStealingQueue(forceCreate: <span class="hljs-literal">true</span>)!<br>    .LocalPush(item, <span class="hljs-keyword">ref</span> _emptyToNonEmptyListTransitionCount);<br><br><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">TryTake</span>(<span class="hljs-params">[MaybeNullWhen(<span class="hljs-literal">false</span></span>)] <span class="hljs-keyword">out</span> T result)</span><br>&#123;<br>    WorkStealingQueue? queue = GetCurrentThreadWorkStealingQueue(forceCreate: <span class="hljs-literal">false</span>);<br>    <span class="hljs-keyword">return</span> (queue != <span class="hljs-literal">null</span> &amp;&amp; queue.TryLocalPop(<span class="hljs-keyword">out</span> result)) || TrySteal(<span class="hljs-keyword">out</span> result, take: <span class="hljs-literal">true</span>);<br>&#125;<br></code></pre></td></tr></table></figure><p><code>ConcurrentBag&lt;T&gt;</code>为了达到线程尽量不争抢的目的，使用了线程局部存储 <em>(Thread-Local Storage)</em>。</p><ul><li>当一个线程调用<code>Add</code>方法时，它会将元素添加到该线程的本地队列中，这样就避免了多个线程之间的锁竞争。</li><li>当一个线程调用<code>TryTake</code>方法时，它首先尝试从自己的本地队列中取出一个元素，如果本地队列为空，则会尝试从其他线程的队列中<strong>偷</strong>一个元素。</li></ul><h3 id="work-stealing"><a class="markdownIt-Anchor" href="#work-stealing"></a> Work Stealing</h3><p><em>不好翻译，暂时叫做“工作窃取”</em></p><p>从上面的<code>TryTake</code>方法可以看出，如果当前线程的本地队列没有元素可供取出，它会尝试从其他线程的队列中<strong>偷取</strong>一个元素。这种机制被称为<strong>工作窃取</strong> <em>(Work Stealing)</em>，它允许线程在自己的队列为空时，从其他线程的队列中获取任务。</p><p>从定义来看，它会维护一个<code>ThreadLocal&lt;WorkStealingQueue&gt;</code>类型的字段<code>_locals</code>，每个线程都会有一个独立的<code>WorkStealingQueue</code>实例来存储该线程的元素。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-comment"><span class="hljs-doctag">///</span> The per-bag, per-thread work-stealing queues.</span><br><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> ThreadLocal&lt;WorkStealingQueue&gt; _locals;<br><span class="hljs-comment">// The head work stealing queue in a linked list of queues.</span><br><span class="hljs-keyword">private</span> <span class="hljs-keyword">volatile</span> WorkStealingQueue? _workStealingQueues;<br></code></pre></td></tr></table></figure><p>让我们从带有冗长注释的<code>TrySteal</code>方法的实现来看看它是如何保证窃取的：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">TrySteal</span>(<span class="hljs-params">[MaybeNullWhen(<span class="hljs-literal">false</span></span>)] <span class="hljs-keyword">out</span> T result, <span class="hljs-built_in">bool</span> take)</span><br>&#123;<br>    <span class="hljs-comment">// ...</span><br>    <span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>)<br>    &#123;<br>        <span class="hljs-comment">// 小巧思1：处理伪空队列的情况，避免在窃取过程中出现竞争条件</span><br>        <span class="hljs-built_in">long</span> initialEmptyToNonEmptyCounts = Interlocked.Read(<span class="hljs-keyword">ref</span> _emptyToNonEmptyListTransitionCount);<br><br>        WorkStealingQueue? localQueue = GetCurrentThreadWorkStealingQueue(forceCreate: <span class="hljs-literal">false</span>);<br><br>        <span class="hljs-comment">// 小巧思2：先尝试从本地队列的下一个队列中窃取，减少多个线程同时检查同一个队列的竞争</span><br>        <span class="hljs-keyword">if</span> (localQueue <span class="hljs-keyword">is</span> <span class="hljs-literal">null</span> ?<br>            TryStealFromTo(_workStealingQueues, <span class="hljs-literal">null</span>, <span class="hljs-keyword">out</span> result, take) :<br>            (TryStealFromTo(localQueue._nextQueue, <span class="hljs-literal">null</span>, <span class="hljs-keyword">out</span> result, take) || TryStealFromTo(_workStealingQueues, localQueue, <span class="hljs-keyword">out</span> result, take)))<br>        &#123;<br>            <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;<br>        &#125;<br><br>        <span class="hljs-keyword">if</span> (Interlocked.Read(<span class="hljs-keyword">ref</span> _emptyToNonEmptyListTransitionCount) == initialEmptyToNonEmptyCounts)<br>        &#123;<br>            <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>在原文的注释表述中，工程师描述了一个竞态场景：</p><ul><li>整个 Bag 里原本只有 1 个元素，在线程 A 手里。</li><li>线程 B 进来想取数据，发现本地为空，准备去偷。</li><li>线程 B 检查线程 C，发现空的；准备检查线程 A。</li><li>就在这时，线程 D 往自己的空队列里加了 1 个元素（现在总数是 2）。</li><li>线程 A 把自己唯一的元素处理掉了（现在总数是 1）。</li><li>线程 B 继续执行，检查线程 A，发现空的。</li><li>结果：线程 B 认为没东西了，但实际上在整个过程中，Bag 里始终至少有 1 个元素。</li></ul><p>为了避免这种情况，<code>TrySteal</code>方法在每次尝试窃取之前，都会读取一个名为<code>_emptyToNonEmptyListTransitionCount</code>的计数器，这个计数器会在每次从空状态变为非空状态时增加。当线程 B 发现没有元素可窃取时，它会再次检查这个计数器，如果它的值没有改变，说明在尝试窃取的过程中没有新的元素被添加到 Bag 中，因此可以安全地返回 <code>false</code>。如果计数器的值发生了变化，说明在尝试窃取的过程中有新的元素被添加到 Bag 中，线程 B 就会继续尝试窃取。</p><p>同时，<code>TrySteal</code>方法还会先尝试从当前线程的下一个队列中窃取，这样可以减少多个线程同时检查同一个队列的竞争，提高窃取的效率。</p><h3 id="昂贵的-count"><a class="markdownIt-Anchor" href="#昂贵的-count"></a> 昂贵的 Count</h3><p>观察<code>ConcurrentBag&lt;T&gt;</code>的<code>Count</code>属性：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> Count<br>&#123;<br>    <span class="hljs-keyword">get</span><br>    &#123;<br>        <span class="hljs-comment">// 短路：如果没有任何队列，直接返回 0</span><br>        <span class="hljs-keyword">if</span> (_workStealingQueues == <span class="hljs-literal">null</span>)<br>        &#123;<br>            <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>        &#125;<br><br>        <span class="hljs-built_in">bool</span> lockTaken = <span class="hljs-literal">false</span>;<br>        <span class="hljs-keyword">try</span><br>        &#123;<br>            <span class="hljs-comment">// 冻结整个 Bag，防止在计算 Count 的过程中有新的元素被添加或移除，保证 Count 的准确性</span><br>            FreezeBag(<span class="hljs-keyword">ref</span> lockTaken);<br>            <span class="hljs-keyword">return</span> DangerousCount;<br>        &#125;<br>        <span class="hljs-keyword">finally</span><br>        &#123;<br>            <span class="hljs-comment">// 解冻 Bag，允许其他线程继续添加或移除元素</span><br>            UnfreezeBag(lockTaken);<br>        &#125;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>所以，<code>ConcurrentBag&lt;T&gt;</code>的<code>Count</code>属性是一个昂贵的操作，这种设计使得在高并发场景下频繁访问 <code>Count</code> 可能会导致性能问题。如果需要判断是否有值，可以使用<code>IsEmpty</code>属性，它实现了一些 Fast-path，通常比<code>Count</code>更高效。</p><h3 id="适用场景"><a class="markdownIt-Anchor" href="#适用场景"></a> 适用场景</h3><p>它更适合<strong>同一个线程</strong>既是生产者又是消费者的场景，比如<code>Parallel.For</code>循环中的局部数据收集器。对于多个线程之间共享数据的场景，由于<code>ConcurrentBag&lt;T&gt;</code>更容易触发窃取，可能会导致性能下降，建议使用<code>ConcurrentQueue&lt;T&gt;</code>或<code>ConcurrentStack&lt;T&gt;</code>等其他线程安全集合类型。</p><p><em>如果真的是遇到生产者消费者场景，建议使用<code>System.Threading.Channels</code>命名空间下的<code>Channel&lt;T&gt;</code>，它提供了更高效、功能更丰富的生产者-消费者数据结构。</em></p><h2 id="immutablelist"><a class="markdownIt-Anchor" href="#immutablelist"></a> ImmutableList</h2><p><code>System.Collections.Immutable.ImmutableList&lt;T&gt;</code>是一个不可变的线性表实现，提供了线程安全的读写操作。它内部使用了<strong>持久化数据结构</strong> <em>(Persistent Data Structure)</em> 的设计理念，通过共享未修改的部分来实现高效的内存使用和性能。</p><p>任何尝试修改<code>ImmutableList&lt;T&gt;</code>的方法（如<code>Add</code>、<code>Remove</code>等）都会返回一个新的<code>ImmutableList&lt;T&gt;</code>实例，而原始实例保持不变。这种设计使得<code>ImmutableList&lt;T&gt;</code>适合在多线程环境中使用，因为它不需要担心数据竞争和同步问题。不同的是<code>ImmutableList&lt;T&gt;</code>没有构造函数，而是通过静态工厂方法来创建实例，如<code>ImmutableList.Create&lt;T&gt;()</code>。</p><p>和<a href="/2026/dotnet-data-structure-key-value-pair">另一篇文章</a>中提到的<code>ImmutableDictionary&lt;TKey, TValue&gt;</code>一样，<code>ImmutableList&lt;T&gt;</code>的实现也是基于 AVL 树的，提供了<code>O(log n)</code>的访问和修改性能，这就是需要性能妥协的地方。</p><p><em>说真的，整个<code>Immutable</code>我很少见过，从它的特性，理论上的话…肯定有足够有理由去使用它，但实际项目中我还没有遇到过。</em></p><h2 id="span-和-memory"><a class="markdownIt-Anchor" href="#span-和-memory"></a> Span 和 Memory</h2><p>从 .NET Core 2.1 开始，微软引入了一个新的线性表数据结构，它是一个<strong>内存切片</strong> <em>(Memory Slice)</em>，提供了对连续内存区域的高效访问。<code>Span&lt;T&gt;</code>是<code>ref struct</code>，所以可以在栈上分配，也可以引用托管堆上的数组或非托管内存，因此它具有非常低的内存分配和 GC 压力。</p><p><code>Span&lt;T&gt;</code>仅有两个字段：</p><ul><li><code>ref T _reference</code> 一个能够直接指向对象内存布局内某个偏移量的引用，或者指向非托管内存的指针。</li><li><code>int _length</code> 表示<code>Span&lt;T&gt;</code>的元素数量。</li></ul><p>它象征着 .NET 平台在开销上最极致的线性表实现（包括<code>ReadOnlySpan&lt;T&gt;</code>），适用于性能敏感的场景，如高性能计算、图像处理、网络编程等。</p><h3 id="从字符串开始"><a class="markdownIt-Anchor" href="#从字符串开始"></a> 从字符串开始</h3><p>其实我最早接触到<code>Span&lt;T&gt;</code>是在字符串处理的场景中，尤其是<code>string.AsSpan()</code>方法，它允许我们在不创建新的字符串实例的情况下，对字符串进行切片和操作。比如从一个字符串中取一段子字符串：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-comment">// 使用 Substring</span><br><span class="hljs-built_in">string</span> data = <span class="hljs-string">&quot;TIMESTAMP:2026-03-04;SENSOR_ID:1145;VALUE:14&quot;</span>;<br><span class="hljs-built_in">string</span> valueStr = data.Substring(data.IndexOf(<span class="hljs-string">&quot;VALUE:&quot;</span>) + <span class="hljs-number">6</span>);<br><span class="hljs-built_in">double</span> <span class="hljs-keyword">value</span> = <span class="hljs-built_in">double</span>.Parse(valueStr);<br><br><span class="hljs-comment">// 使用 AsSpan</span><br><span class="hljs-built_in">string</span> data = <span class="hljs-string">&quot;TIMESTAMP:2026-03-04;SENSOR_ID:1145;VALUE:14&quot;</span>;<br>ReadOnlySpan&lt;<span class="hljs-built_in">char</span>&gt; span = data.AsSpan(data.IndexOf(<span class="hljs-string">&quot;VALUE:&quot;</span>) + <span class="hljs-number">6</span>);<br><span class="hljs-built_in">double</span> <span class="hljs-keyword">value</span> = <span class="hljs-built_in">double</span>.Parse(span);<br></code></pre></td></tr></table></figure><p>观察<code>MemoryExtensions</code>类中的<code>AsSpan</code>方法：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-title">ReadOnlySpan</span>&lt;<span class="hljs-title">char</span>&gt; <span class="hljs-title">AsSpan</span>(<span class="hljs-params"><span class="hljs-keyword">this</span> <span class="hljs-built_in">string</span>? text, <span class="hljs-built_in">int</span> start</span>)</span><br>&#123;<br>    <span class="hljs-comment">// ...</span><br>    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ReadOnlySpan&lt;<span class="hljs-built_in">char</span>&gt;(<span class="hljs-keyword">ref</span> Unsafe.Add(<span class="hljs-keyword">ref</span> text.GetRawStringData(), (<span class="hljs-built_in">nint</span>)(<span class="hljs-built_in">uint</span>)start, text.Length - start);<br>&#125;<br></code></pre></td></tr></table></figure><p><code>AsSpan</code>方法通过使用<code>Unsafe.Add</code>来创建一个新的<code>ReadOnlySpan&lt;char&gt;</code>实例，其中<code>GetRawStringData()</code>是拿到字符串内部的<code>ref char GetRawStringData() =&gt; ref _firstChar;</code>，这个字段是字符串内部的第一个字符的引用，所以<code>AsSpan</code>方法实际上是创建了一个指向原始字符串数据的切片,它直接<strong>引用</strong>了原始字符串的数据，而没有进行任何复制操作。</p><h3 id="内存秦始皇"><a class="markdownIt-Anchor" href="#内存秦始皇"></a> 内存秦始皇</h3><p>对于流、缓冲区等需要频繁分配和释放内存的场景，<code>Span&lt;T&gt;</code>提供了一个统一的内存访问抽象，可以直接操作连续的内存区域，而不需要担心内存分配和 GC 的开销。曾经，可能需要这么写：</p><ul><li>字节数组：<code>Method(byte[] buffer)</code></li><li>字符串：<code>Method(string data)</code></li><li>非托管内存：<code>Method(IntPtr ptr, int length)</code></li></ul><p>有了<code>Span&lt;T&gt;</code>，我们可以统一使用<code>Method(Span&lt;byte&gt; buffer)</code>来处理所有这些场景，比如<code>FileStream</code>的<code>Read</code>和<code>Write</code>方法：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-comment">// 读取到 Span&lt;byte&gt;</span><br><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> <span class="hljs-built_in">int</span> <span class="hljs-title">Read</span>(<span class="hljs-params">Span&lt;<span class="hljs-built_in">byte</span>&gt; buffer</span>)</span> =&gt; _strategy.Read(buffer);<br><span class="hljs-comment">// 将 ReadOnlySpan&lt;byte&gt; 写入到文件</span><br><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Write</span>(<span class="hljs-params">ReadOnlySpan&lt;<span class="hljs-built_in">byte</span>&gt; buffer</span>)</span> =&gt; _strategy.Write(buffer);<br></code></pre></td></tr></table></figure><h3 id="限制"><a class="markdownIt-Anchor" href="#限制"></a> 限制</h3><p>由于<code>Span&lt;T&gt;</code>是一个<code>ref struct</code>，它有一些限制，比如：</p><ul><li>只能在栈上分配，不能作为字段存储在类中。</li><li>不能被装箱，不能作为接口类型使用。</li></ul><p>所以，<code>Memory&lt;T&gt;</code>是<code>Span&lt;T&gt;</code>的一个引用类型版本，提供了类似的功能，但可以在堆上分配，并且可以作为字段存储在类中。<code>Memory&lt;T&gt;</code>内部持有一个<code>object</code>类型的引用和一个偏移量，可以引用托管堆上的数组或非托管内存。它的性能略低于<code>Span&lt;T&gt;</code>，因为需要额外封装开销。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs csharp">Memory&lt;<span class="hljs-built_in">byte</span>&gt; buffer = <span class="hljs-keyword">new</span> <span class="hljs-built_in">byte</span>[<span class="hljs-number">1024</span>]; <span class="hljs-comment">// 分配字节缓冲区</span><br>Span&lt;<span class="hljs-built_in">byte</span>&gt; spanView = buffer.Span.Slice(<span class="hljs-number">0</span>,<span class="hljs-number">100</span>); <span class="hljs-comment">// 获取前100字节的 Span</span><br></code></pre></td></tr></table></figure><p>比如在<code>System.IO.Pipelines</code>命名空间中，都会使用<code>Memory&lt;byte&gt;</code>来处理数据缓冲区和零拷贝的数据传递。它在最大程度保留<code>Span&lt;T&gt;</code>性能优势的同时，更灵活地在异步和泛型等场景中使用。</p><h2 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h2><p>从最初的非泛型 <code>ArrayList</code> 到泛型的 <code>List&lt;T&gt;</code>、<code>ReadOnlyCollection&lt;T&gt;</code>、<code>ConcurrentBag&lt;T&gt;</code>、再到真正的不可变集合<code>ImmutableList&lt;T&gt;</code>和高效的 <code>Span/Memory</code> 类型，C#/.NET 的线性表结构不断演进以满足类型安全、性能优化和并发编程等需求。</p>]]>
    </content>
    <id>https://ziling.moe/2026/dotnet-data-structure-linear-list/</id>
    <link href="https://ziling.moe/2026/dotnet-data-structure-linear-list/"/>
    <published>2026-03-04T04:00:00.000Z</published>
    <summary>线性表数据结构的演进。</summary>
    <title>.NET 数据结构大杂烩：线性表</title>
    <updated>2026-03-04T04:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term=".NET Learning" scheme="https://ziling.moe/categories/NET-Learning/"/>
    <category term=".NET" scheme="https://ziling.moe/tags/NET/"/>
    <category term="数据结构" scheme="https://ziling.moe/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    <content>
      <![CDATA[<p>在工程场景中，我们经常会借助<strong>键值对</strong> <em>(Key-Value Pair)</em> 和<strong>哈希</strong> <em>(Hash)</em> 的机制来操作数据，尤其是面对关系映射、缓存、对象池等场景中，它们的存在十分必要。</p><p>在 .NET 的历史中，该数据结构也经历了多次演进。它们的实现原理很简单，但随着性能、内存、多线程等方面的需求不断提升，.NET 也在不断优化和改进这些数据结构的实现。</p><p><em>本文寻找的源代码均为最新，比如曾经的<code>object</code>可能变成了<code>object?</code>，但这并不影响我们对其设计和实现的理解。</em></p><h2 id="hashtable"><a class="markdownIt-Anchor" href="#hashtable"></a> Hashtable</h2><p>在 .NET 平台的初期，C# 1.0 的规范中指定<code>System.Collections.Hashtable</code>是处理键值对映射的唯一官方组件。虽然它通常情况下时间复杂度为 O(1)，但局限于当时的设计和实现，对比于接下来的 <code>Dictionary&lt;TKey, TValue&gt;</code>，它存在以下显著的缺点。</p><h3 id="类型安全问题"><a class="markdownIt-Anchor" href="#类型安全问题"></a> 类型安全问题</h3><p>找到<code>Hashtable</code><a href="https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Collections/Hashtable.cs,10fefb6e0ae510dd">源代码</a>的<code>Add</code>方法，会发现它接受的参数类型为 <code>object</code>。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Add</span>(<span class="hljs-params"><span class="hljs-built_in">object</span> key, <span class="hljs-built_in">object</span>? <span class="hljs-keyword">value</span></span>)</span><br>&#123;<br>    Insert(key, <span class="hljs-keyword">value</span>, <span class="hljs-literal">true</span>);<br>&#125;<br></code></pre></td></tr></table></figure><p>因为你可以将任何类型的对象放入同一个<code>Hashtable</code>中，这就导致在尝试取出其数据时，必须进行<strong>显式类型转换</strong>，如果转换失败，就会抛出运行时错误。</p><p>所以<code>Hashtable</code>无法保证编译时类型安全，容易引发运行时错误，但这并不是它本身的问题，因为泛型是在 C# 2.0 引入的，这个问题在当时确实无法避免。</p><details class="tag-plugin colorful folding" color="orange" open><summary><p>不建议使用</p></summary><div class="body"><p>现如今，新项目建议直接使用<code>Dictionary&lt;TKey, TValue&gt;</code>，因为<code>Hashtable</code>缺少泛型带来的好处，详见<a href="https://github.com/dotnet/platform-compat/blob/master/docs/DE0006.md">DE0006: Non-generic collections shouldn’t be used</a></p> </div></details><h3 id="装箱与拆箱的性能问题"><a class="markdownIt-Anchor" href="#装箱与拆箱的性能问题"></a> 装箱与拆箱的性能问题</h3><p>上述的类型安全问题同时引入了<code>object</code>类型的装箱和拆箱操作，尤其是对于值类型（如<code>int</code>、<code>double</code>等）来说，这些操作会导致性能下降和 GC 压力增加。</p><p>尽管在如今的 .NET 版本中，JIT 编译器已经对装箱和拆箱进行了优化，但在当时的环境中，这确实是一个显著的性能问题（现在同样也强烈建议避免装箱和拆箱操作）。</p><h3 id="可读性和维护性问题"><a class="markdownIt-Anchor" href="#可读性和维护性问题"></a> 可读性和维护性问题</h3><p>同上，缺乏泛型约束的<code>Hashtable</code>使得代码的意图不够明确。对比<code>Dictionary&lt;TKey, TValue&gt;</code>，它的类型参数明确了键和值的类型，而<code>Hashtable</code>则需要开发者在使用前确认其内容的类型，且在使用过程中也需要频繁进行类型检查和转换。</p><p>如果你尝试在<code>Hashtable</code>访问一个不存在的键，返回的结果是<code>null</code>，虽然谈不上是缺点，但是需要在约定上总是检查返回值是否为<code>null</code>，否则可能会引发<code>NullReferenceException</code>。</p><h3 id="底层实现"><a class="markdownIt-Anchor" href="#底层实现"></a> 底层实现</h3><p><code>Hashtable</code>采用了开放寻址法 <em>(Open Addressing)</em> 中的双重散列 <em>(Double Hashing)</em> 来实现。</p><p>观察它的存储结构，其内部维护了一个<code>bucket</code>结构体数组，包含三个字段。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">private</span> <span class="hljs-keyword">struct</span> Bucket<br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">object</span>? key;   <span class="hljs-comment">// 键</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">object</span>? val;   <span class="hljs-comment">// 值</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> hash_coll; <span class="hljs-comment">// 存储哈希值的同时记录是否发生过碰撞，详解见后文</span><br>&#125;<br><br><span class="hljs-keyword">private</span> Bucket[] _buckets = <span class="hljs-literal">null</span>!;<br></code></pre></td></tr></table></figure><h3 id="计算过程"><a class="markdownIt-Anchor" href="#计算过程"></a> 计算过程</h3><p>当我们向<code>Hashtable</code>添加一个键值对时，它会按照以下步骤进行：</p><p><code>Hashtable</code>的核心探测公式是：</p><p class='katex-block'><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>h</mi><mo stretchy="false">(</mo><mi>k</mi><mi>e</mi><mi>y</mi><mo separator="true">,</mo><mi>n</mi><mo stretchy="false">)</mo><mo>=</mo><msub><mi>h</mi><mn>1</mn></msub><mo stretchy="false">(</mo><mi>k</mi><mi>e</mi><mi>y</mi><mo stretchy="false">)</mo><mo>+</mo><mi>n</mi><mo>⋅</mo><msub><mi>h</mi><mn>2</mn></msub><mo stretchy="false">(</mo><mi>k</mi><mi>e</mi><mi>y</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">h(key, n) = h_1(key) + n \cdot h_2(key)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal">h</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.16666666666666666em;"></span><span class="mord mathnormal">n</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">h</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.30110799999999993em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:0.44445em;vertical-align:0em;"></span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">h</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.30110799999999993em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mclose">)</span></span></span></span></span></p><ul><li><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>h</mi><mn>1</mn></msub><mo stretchy="false">(</mo><mi>k</mi><mi>e</mi><mi>y</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">h_1(key)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">h</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.30110799999999993em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mclose">)</span></span></span></span>：初始落点（基准位置）</li><li><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>h</mi><mn>2</mn></msub><mo stretchy="false">(</mo><mi>k</mi><mi>e</mi><mi>y</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">h_2(key)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">h</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.30110799999999993em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mclose">)</span></span></span></span>：每次发生碰撞时，计算新的探测位置的增量（步长）</li><li><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi></mrow><annotation encoding="application/x-tex">n</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.43056em;vertical-align:0em;"></span><span class="mord mathnormal">n</span></span></span></span>：当前是第几次碰撞探测（循环次数）</li></ul><p>这里其实引出了一个数学表达和工程实现的差异。</p><p>站在数学的角度看，第<code>n</code>次探测的绝对位置公式是 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>P</mi><mo stretchy="false">(</mo><mi>n</mi><mo stretchy="false">)</mo><mo>=</mo><mo stretchy="false">(</mo><msub><mi>h</mi><mn>1</mn></msub><mo stretchy="false">(</mo><mi>k</mi><mi>e</mi><mi>y</mi><mo stretchy="false">)</mo><mo>+</mo><mi>n</mi><mo>⋅</mo><msub><mi>h</mi><mn>2</mn></msub><mo stretchy="false">(</mo><mi>k</mi><mi>e</mi><mi>y</mi><mo stretchy="false">)</mo><mo stretchy="false">)</mo><mspace></mspace><mspace width="0.4444444444444444em"/><mo stretchy="false">(</mo><mrow><mi mathvariant="normal">m</mi><mi mathvariant="normal">o</mi><mi mathvariant="normal">d</mi></mrow><mspace width="0.3333333333333333em"/><mi>m</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">P(n) = (h_1(key) + n \cdot h_2(key)) \pmod m</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.13889em;">P</span><span class="mopen">(</span><span class="mord mathnormal">n</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal">h</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.30110799999999993em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:0.44445em;vertical-align:0em;"></span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">h</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.30110799999999993em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mclose">)</span><span class="mclose">)</span><span class="mspace allowbreak"></span><span class="mspace" style="margin-right:0.4444444444444444em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord"><span class="mord"><span class="mord mathrm">m</span><span class="mord mathrm">o</span><span class="mord mathrm">d</span></span></span><span class="mspace" style="margin-right:0.3333333333333333em;"></span><span class="mord mathnormal">m</span><span class="mclose">)</span></span></span></span>，因为连续的加法就是乘法，每次迭代都会增加一个步长 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>h</mi><mn>2</mn></msub><mo stretchy="false">(</mo><mi>k</mi><mi>e</mi><mi>y</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">h_2(key)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">h</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.30110799999999993em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mclose">)</span></span></span></span>，所以我们可以直接用乘法来表达。</p><p>但是从工程实现的角度，我们不能简单地将这个迭代改成循环，然后每次循环都执行一次 <code>n * h2</code> 的乘法运算，在极高频调用场景中，这么做的性能开销很大。</p><p>实际上，每次循环之后的位置是上一次位置加上步长，所以我们可以直接在循环中维护一个当前探测位置的变量，每次发生碰撞时直接加上步长，这样就避免了每次都进行乘法运算。</p><p>注释中提到：</p><blockquote><p>We previously used a different h2(key, n) that was not constant. That is ahorrifically bad idea …</p></blockquote><p>有可能在更早期的实现中，<code>h2</code>是一个非恒定的函数，可能会根据<code>n</code>的值进行变化，比如二次探测 <em>(Quadratic Probing)</em>，尽管更复杂的公式能够让哈希散布更均匀，但性能开销也会更大，不如退回到一个恒定的步长来得高效。</p><hr /><p>回到这个公式本身，先计算 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>h</mi><mn>1</mn></msub><mo stretchy="false">(</mo><mi>k</mi><mi>e</mi><mi>y</mi><mo stretchy="false">)</mo><mo>=</mo><mi>G</mi><mi>e</mi><mi>t</mi><mi>H</mi><mi>a</mi><mi>s</mi><mi>h</mi><mo stretchy="false">(</mo><mi>k</mi><mi>e</mi><mi>y</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">h_1(key) = GetHash(key)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">h</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.30110799999999993em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal">G</span><span class="mord mathnormal">e</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.08125em;">H</span><span class="mord mathnormal">a</span><span class="mord mathnormal">s</span><span class="mord mathnormal">h</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mclose">)</span></span></span></span> ，来决定该元素在理想情况下应当存储的位置。</p><p>但如果此时该位置被占用，就需要进行第二步，计算步长，尝试寻找下一个位置。计算公式如下：</p><p class='katex-block'><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>h</mi><mn>2</mn></msub><mo stretchy="false">(</mo><mi>k</mi><mi>e</mi><mi>y</mi><mo stretchy="false">)</mo><mo>=</mo><mn>1</mn><mo>+</mo><mo stretchy="false">(</mo><mo stretchy="false">(</mo><mo stretchy="false">(</mo><msub><mi>h</mi><mn>1</mn></msub><mo stretchy="false">(</mo><mi>k</mi><mi>e</mi><mi>y</mi><mo stretchy="false">)</mo><mo>≫</mo><mn>5</mn><mo stretchy="false">)</mo><mo>+</mo><mn>1</mn><mo stretchy="false">)</mo><mspace></mspace><mspace width="1em"/><mo stretchy="false">(</mo><mrow><mi mathvariant="normal">m</mi><mi mathvariant="normal">o</mi><mi mathvariant="normal">d</mi></mrow><mspace width="0.3333333333333333em"/><mtext>hashsize</mtext><mo>−</mo><mn>1</mn><mo stretchy="false">)</mo><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">h_2(key) = 1 + (((h_1(key) \gg 5) + 1) \pmod{\text{hashsize} - 1})</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">h</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.30110799999999993em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:0.72777em;vertical-align:-0.08333em;"></span><span class="mord">1</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mopen">(</span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal">h</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.30110799999999993em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span><span class="mrel">≫</span><span class="mspace" style="margin-right:0.2777777777777778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord">5</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord">1</span><span class="mclose">)</span><span class="mspace allowbreak"></span><span class="mspace" style="margin-right:1em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord"><span class="mord"><span class="mord mathrm">m</span><span class="mord mathrm">o</span><span class="mord mathrm">d</span></span></span><span class="mspace" style="margin-right:0.3333333333333333em;"></span><span class="mord text"><span class="mord">hashsize</span></span><span class="mspace" style="margin-right:0.2222222222222222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222222222222222em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord">1</span><span class="mclose">)</span><span class="mclose">)</span></span></span></span></span></p><p>其中：</p><ul><li>右移5位的意图在于将原哈希值较高位的部分参与到这一轮计算中，以保证即使 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>h</mi><mn>1</mn></msub><mo stretchy="false">(</mo><mi>k</mi><mi>e</mi><mi>y</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">h_1(key)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">h</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.30110799999999993em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mclose">)</span></span></span></span> 碰撞，它们的 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>h</mi><mn>2</mn></msub><mo stretchy="false">(</mo><mi>k</mi><mi>e</mi><mi>y</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">h_2(key)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">h</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.30110799999999993em;"><span style="top:-2.5500000000000003em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mclose">)</span></span></span></span> 也大概率不同，从而减少连续碰撞的可能性。</li><li>取模的目的在于确保步长的最大值不会超出数组总长度。</li><li>加1是为了确保步长至少为1，避免原地踏步。</li></ul><p>这里的<code>hashsize</code>藏了一个小巧思，源码注释提到：</p><blockquote><p>2 must return a number between 1 and hashsize - 1 that is relatively prime to hashsize (not a problem if hashsize is prime).<br />(Knuth’s Art of Computer Programming, Vol. 3, p. 528-9)</p></blockquote><p>假设<code>hashsize</code>为10，步长为2，探测序列将是：<code>0, 2, 4, 6, 8, 0, 2, ...</code>，另一半空间将永远无法被访问到，这就导致了内存浪费。因此，<code>hashsize</code>必须是一个质数，以确保步长与数组长度互质，从而能够访问到数组中的每一个位置。</p><hr /><p>还记得<code>Bucket</code>结构体中的<code>hash_coll</code>字段吗？</p><p>第一个问题是为什么要标记碰撞，因为在开放寻址法中，如果发生碰撞，我们需要继续探测下一个位置，直到找到一个空位或者找到目标键。如果没有标记碰撞的信息，我们就无法区分当前桶是空的还是被占用但发生了碰撞，这会导致探测过程中的错误判断。</p><p>同样，在查找的探测过程中，如果在某个位置发现这个键不匹配的同时没有发生冲突，就意味着这个键根本不存在于哈希表中，可以直接返回未找到的结果；如果发生了碰撞，就需要继续探测下一个位置，直到找到匹配的键或者遇到一个没有发生碰撞的空桶。</p><p>其次，可能会疑惑为什么要将哈希值和碰撞信息存储在同一个字段中？不拆开放，一个<code>int hashCode</code>和一个<code>bool hasCollision</code>不就好了。</p><p>首先是内存占用优化。一个<code>int</code>有32位，但哈希值不需要用满32位，最高位可以用来标记是否发生过碰撞，这样就节省了一个<code>bool</code>的内存空间，同时保证哈希值为正数即可。</p><p>部分源代码和注释如下：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-comment">// Hashcode must be positive.  Also, we must not use the sign bit, since</span><br><span class="hljs-comment">// that is used for the collision bit.</span><br><span class="hljs-built_in">uint</span> hashcode = (<span class="hljs-built_in">uint</span>)GetHash(key) &amp; <span class="hljs-number">0x7FFFFFFF</span>;<br></code></pre></td></tr></table></figure><hr /><p>最后就是保证删除元素后哈希表的完整性问题了。开放寻址法中，如果直接将一个桶标记为删除，那么在后续的探测过程中，如果遇到这个被标记为删除的桶，就会误以为这个键不存在，从而导致查找失败。</p><p>因此，<code>Hashtable</code>在删除元素时，并不是直接将桶标记为删除，而是将其标记为发生过碰撞的状态，这样在后续的探测过程中就会继续探测下一个位置，直到找到匹配的键或者遇到一个没有发生碰撞的空桶。</p><h2 id="dictionary"><a class="markdownIt-Anchor" href="#dictionary"></a> Dictionary</h2><p>2005年，随着 .NET Framework 2.0 的发布，微软引入了泛型 <em>(Generics)</em>，并基于此推出了<code>Dictionary&lt;TKey, TValue&gt;</code>，它在设计上解决了<code>Hashtable</code>的诸多问题，成为了新的键值对数据结构的标准实现。直到现在，<code>Dictionary&lt;TKey, TValue&gt;</code>仍然是 .NET 中最常用的键值对数据结构之一。</p><p><em>后文将使用<code>Dictionary</code>来指代<code>Dictionary&lt;TKey, TValue&gt;</code>，以简化表述。</em></p><p>与<code>Hashtable</code>不同，<code>Dictionary</code>采用了<strong>链式地址法</strong> <em>(Chaining)</em> 的方式来处理碰撞问题。同时，为了避免传统链表在堆上离散分配内存锁导致的内存碎片化和性能问题，工程师改用了<strong>数组+索引</strong>的方式来实现链表结构。</p><p>观察<code>Dictionary</code>的存储结构，其内部维护了一个<code>Entry</code>结构体数组，包含四个字段。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">private</span> Entry[]? _entries;<br><br><span class="hljs-keyword">private</span> <span class="hljs-keyword">struct</span> Entry<br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">uint</span> hashCode;  <span class="hljs-comment">// 哈希值</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> next;       <span class="hljs-comment">// 下一个元素在数组中的索引</span><br>    <span class="hljs-keyword">public</span> TKey key;       <span class="hljs-comment">// 键</span><br>    <span class="hljs-keyword">public</span> TValue <span class="hljs-keyword">value</span>;   <span class="hljs-comment">// 值</span><br>&#125;<br></code></pre></td></tr></table></figure><p>这种数据结构的设计使得其自身在内存布局上更为紧凑。当哈希碰撞发生时，元素之间也不是通过对象引用连接，而是通过<code>next</code>字段在数组内部形成一个基于索引的单向链表。对于 CPU 来说，读取相邻的内存块总是更快的。</p><h3 id="优势"><a class="markdownIt-Anchor" href="#优势"></a> 优势</h3><p><code>Dictionary</code>是一个泛型类，定义了类型参数<code>TKey</code>和<code>TValue</code>，这使得它在编译时就能够保证类型安全。开发者在使用<code>Dictionary</code>时必须指定键和值的类型，这样在编译阶段就能捕获类型错误，避免了运行时错误的风险。</p><p>由于泛型参数在运行时会被实例化为对应的内存布局，当使用<code>int</code>等值类型作为键时，<code>entries</code>数组中的<code>key</code>字段会直接存储<code>int</code>的值，这个过程中不会出现堆内存分配，也就不存在装箱和拆箱的性能问题了。</p><p>在两年后的 C# 3.0 中，微软引入了 LINQ <em>(Language-Integrated Query)</em>，<code>Dictionary</code>也实现了<code>IEnumerable&lt;KeyValuePair&lt;TKey, TValue&gt;&gt;</code>接口，使得我们可以使用 LINQ 来查询和操作字典中的数据。</p><h4 id="空安全设计"><a class="markdownIt-Anchor" href="#空安全设计"></a> 空安全设计</h4><p><code>TryXXX</code> 方法一直是我喜欢的设计模式，<code>Dictionary</code>提供了<code>TryGetValue</code>方法来安全地尝试获取一个键对应的值，而不会抛出异常。这种设计使得代码更加健壮和易于维护。</p><p>对于以下示例：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">var</span> dict = <span class="hljs-keyword">new</span> Dictionary&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">int</span>&gt;();<br>dict[<span class="hljs-string">&quot;a&quot;</span>] = <span class="hljs-number">1</span>;<br><span class="hljs-keyword">if</span> (dict.TryGetValue(<span class="hljs-string">&quot;b&quot;</span>, <span class="hljs-keyword">out</span> <span class="hljs-keyword">var</span> <span class="hljs-keyword">value</span>))  <span class="hljs-comment">// 尝试获取键 &quot;b&quot; 的值，如果存在则设置 value 并返回 true，否则返回 false</span><br>    Console.WriteLine(<span class="hljs-keyword">value</span>);<br><span class="hljs-keyword">else</span><br>    Console.WriteLine(<span class="hljs-string">&quot;Key not found.&quot;</span>);<br></code></pre></td></tr></table></figure><h3 id="局限性"><a class="markdownIt-Anchor" href="#局限性"></a> 局限性</h3><p>泛型字典的卓越性能和类型安全性使得它成为了 .NET 中的主流键值对数据结构，但它在面对多线程并发读写时并不安全。</p><p>随着多核 CPU 普及，软件架构逐渐需要能够处理高并发需求。在这个时候，开发者可以简单地借助<code>lock</code>关键字来保护对<code>Dictionary</code>的并发访问，但这个锁过于粗粒度，在任何时候只能有一个线程访问字典，这会导致性能瓶颈。</p><p>为了解决这个问题，微软在 .NET 4.0 中引入了<code>ConcurrentDictionary&lt;TKey, TValue&gt;</code>。</p><h2 id="concurrentdictionary"><a class="markdownIt-Anchor" href="#concurrentdictionary"></a> ConcurrentDictionary</h2><p><code>ConcurrentDictionary</code>表示可同时由多个线程访问的键/值对的线程安全集合。</p><p><em>依然管中窥豹，完整的源代码比较长，这里我们只关注它的核心设计和实现。</em></p><p>传统的<code>Dictionary</code>采用的双数组结构（<code>buckets</code>和<code>entries</code>）在多线程环境下底层的索引和链表结构很容易被破坏，从而出现预期之外的数据丢失等问题。</p><p>从源代码的<code>TryAddInternal</code>方法我们能发现<code>ConcurrentDictionary</code>采用了<strong>分段锁</strong> <em>(Segmented Locking)</em> 和无锁读取的设计。</p><h3 id="分段锁机制"><a class="markdownIt-Anchor" href="#分段锁机制"></a> 分段锁机制</h3><p>在该方法的<code>while</code>循环中，存在这段代码结构：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-built_in">object</span>[] locks = tables._locks;<br><span class="hljs-keyword">ref</span> Node? bucket = <span class="hljs-function"><span class="hljs-keyword">ref</span> <span class="hljs-title">GetBucketAndLock</span>(<span class="hljs-params">tables, hashcode, <span class="hljs-keyword">out</span> <span class="hljs-built_in">uint</span> lockNo</span>)</span>;<br><span class="hljs-comment">// ...</span><br>Monitor.Enter(locks[lockNo], <span class="hljs-keyword">ref</span> lockTaken);<br></code></pre></td></tr></table></figure><p>不像<code>lock</code>关键字那样直接锁住整个字典，而是内部维护了一个<code>_locks</code>数组，当计算出键的哈希值后，会根据哈希值计算出一个锁的索引 <code>lockNo</code>，然后只锁住这个特定的锁对象。</p><p>这样，如果两个线程分别访问不同的键，并且这两个键的哈希值映射到不同的锁，那么它们就可以同时进行操作，而不会互相阻塞，从而提高了并发性能。在默认情况下，锁的数量等于 CPU 的逻辑核心数。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;summary&gt;</span>The number of concurrent writes for which to optimize by default.<span class="hljs-doctag">&lt;/summary&gt;</span></span><br><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-built_in">int</span> DefaultConcurrencyLevel =&gt; Environment.ProcessorCount;<br></code></pre></td></tr></table></figure><h3 id="放弃连续数组"><a class="markdownIt-Anchor" href="#放弃连续数组"></a> 放弃连续数组</h3><p>之前提到<code>Dictionary</code>采用了数组+索引的方式来实现链表结构，而<code>ConcurrentDictionary</code>则完全放弃了连续数组的设计，转而回退了<strong>链表节点</strong> <em>(Node)</em> 的方式来存储键值对。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">var</span> resultNode = <span class="hljs-keyword">new</span> Node(key, <span class="hljs-keyword">value</span>, hashcode, bucket);<br>Volatile.Write(<span class="hljs-keyword">ref</span> bucket, resultNode);<br></code></pre></td></tr></table></figure><p>在并发环境下，修改一个连续数组既危险又难以做到无锁安全。而每个<code>Node</code>对象在内存中是分散的，从而可以通过更新引用来安全地替换节点。</p><h3 id="无锁读取"><a class="markdownIt-Anchor" href="#无锁读取"></a> 无锁读取</h3><p>在<code>ConcurrentDictionary</code>中，读取操作是无锁的，这意味着多个线程可以同时读取数据而不会互相阻塞。这个特性是通过使用<code>volatile</code>关键字和内存屏障来实现的，确保了读取操作能够看到最新的写入结果。从源代码的<code>this[key]</code>索引器的<code>get</code>方法中，我们可以看到它直接访问了底层的链表节点，而没有使用任何锁机制。</p><p><code>ConcurrentDictionary</code>中有个字段，<code>volatile Tables _tables</code>，它是一个包含了所有桶和锁的结构体。从这个类的注释可知，<code>Tables</code>保存了<code>ConcurrentDictionary</code>的内部状态，通过将所有可变状态封装到一个单独的对象中，并将其声明为<code>volatile</code>，可以确保在多线程环境下对这个状态的访问是安全的。</p><h3 id="保证原子写入"><a class="markdownIt-Anchor" href="#保证原子写入"></a> 保证原子写入</h3><p>这个方法中同样存在这段逻辑：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">if</span> (!<span class="hljs-keyword">typeof</span>(TValue).IsValueType || ConcurrentDictionaryTypeProps&lt;TValue&gt;.IsWriteAtomic)<br>&#123;<br>    node._value = <span class="hljs-keyword">value</span>;<br>&#125;<br><span class="hljs-keyword">else</span><br>&#123;<br>    <span class="hljs-keyword">var</span> newNode = <span class="hljs-keyword">new</span> Node(node._key, <span class="hljs-keyword">value</span>, hashcode, node._next);<br>    <span class="hljs-keyword">if</span> (prev <span class="hljs-keyword">is</span> <span class="hljs-literal">null</span>)<br>    &#123;<br>        Volatile.Write(<span class="hljs-keyword">ref</span> bucket, newNode);<br>    &#125;<br>    <span class="hljs-keyword">else</span><br>    &#123;<br>        prev._next = newNode;<br>    &#125;<br>&#125;<br>resultingValue = <span class="hljs-keyword">value</span>;<br></code></pre></td></tr></table></figure><p>如果<code>TValue</code>不是值类型，或者是一个值类型但其写入操作是原子的（比如<code>int</code>、<code>long</code>等），那么直接更新节点的值即可；否则，为了保证写入操作的原子性，需要创建一个新的节点来替换旧节点。这样无锁的读线程也不会读到一个不完整的值，从而保证了线程安全。</p><h2 id="immutabledictionary"><a class="markdownIt-Anchor" href="#immutabledictionary"></a> ImmutableDictionary</h2><p>后来，函数式编程再次流行起来，其核心理念之一的<strong>不可变性</strong> <em>(Immutability)</em>，即数据一旦创建，就永远不能被修改的特性，受到了开发者的欢迎。</p><p>与此同时，在多线程环境中传递字典，为了保证传输过程中的防篡改，通常需要复制（深拷贝）整个哈希表，这样的性能开销是非常大的。</p><p>在 .NET Framework 4.5，微软发布了一个独立的<code>System.Collections.Immutable</code> Nuget 包，其中包含了不可变集合的实现，包括<code>ImmutableDictionary&lt;TKey, TValue&gt;</code>。</p><p>该数据结构的设计目标是提供一个线程安全的、不可变的键值对集合。可用于全局配置、只读缓存等一些需要共享但不允许修改的场景。</p><h3 id="转进-avl-树"><a class="markdownIt-Anchor" href="#转进-avl-树"></a> 转进 AVL 树</h3><p><code>ImmutableDictionary</code>的底层实现并不是上述的哈希表，而是基于 <strong>AVL 树</strong> <em>(Adelson-Velsky and Landis Tree)</em> 的一种自平衡二叉搜索树。与哈希表不同，AVL 树通过维护节点之间的高度平衡（左右子树的深度差不超过1）来保证在最坏情况下的 O(log n) 时间复杂度。</p><p><em>所以 <code>ImmutableDictionary</code> 的访问速度在平均情况下是不如哈希表的</em></p><p>AVL 树的设计使其支持更高效的版本控制和状态不变性。在处理哈希碰撞时，节点也不再是简单的链表，而是变成另一棵子树。</p><h3 id="结构共享"><a class="markdownIt-Anchor" href="#结构共享"></a> 结构共享</h3><p>当我们对<code>ImmutableDictionary</code>进行修改（比如添加、删除或更新一个键值对）时，并不会创建一个全新的字典，而是通过<strong>结构共享</strong> <em>(Structural Sharing)</em> 的方式来实现。这意味着新旧字典会共享大部分的内部结构，只有被修改的路径上的节点会被复制和修改。</p><p>当我们向<code>ImmutableDictionary</code>中添加一个新的键值对时，算法会沿着根节点到新节点插入位置的路径，实例化新的分支节点。其他未被修改波及的子树，其引用将直接被旧实例共同持有与共享。同样，这样的一次添加操作的时间复杂度从 O(n) 降低到了 O(log n)，因为只需要复制路径上的节点，而不是整个树。</p><h3 id="局限性-2"><a class="markdownIt-Anchor" href="#局限性-2"></a> 局限性</h3><p>很显然，<code>ImmutableDictionary</code>的访问速度在平均情况下是不如哈希表的，因为它需要进行树的遍历来查找键值对，而哈希表通过计算哈希值可以直接定位到桶的位置。</p><p>同时，它的内存访问模式并不友好，因为树结构的节点在内存中是分散的，这会导致更多的 GC 压力，从而影响性能。</p><h2 id="ordereddictionary"><a class="markdownIt-Anchor" href="#ordereddictionary"></a> OrderedDictionary</h2><p>在实际业务开发中，有可能遇到一个需求：既需要哈希表那种极速键值查找，又必须<strong>保持元素的插入顺序</strong>。</p><p>在很长一段时间里，.NET 只提供了一个非泛型的 <code>System.Collections.Specialized.OrderedDictionary</code>。正如前文对早期 <code>Hashtable</code> 的分析一样，它同样面临着 <code>object</code> 带来的装箱与拆箱性能损耗以及类型安全问题。为了规避这些问题，开发者们往往不得不自己去造轮子，比如在内部同时维护一个 <code>List&lt;T&gt;</code>（用于保序）和一个 <code>Dictionary&lt;TKey, TValue&gt;</code>（用于查找），但这无疑带来了双倍的内存开销和状态同步的复杂性。</p><p>在 .NET 9 中，微软终于在 <code>System.Collections.Generic</code> 命名空间下引入了它的泛型版本 <code>OrderedDictionary&lt;TKey, TValue&gt;</code>。</p><h3 id="兼顾索引与哈希"><a class="markdownIt-Anchor" href="#兼顾索引与哈希"></a> 兼顾索引与哈希</h3><p><code>OrderedDictionary</code> 最大的特点是它同时支持<strong>通过键</strong>和<strong>通过索引</strong>来访问元素：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs csharp">OrderedDictionary&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">int</span>&gt; dict = <span class="hljs-keyword">new</span>()<br>&#123;<br>    [<span class="hljs-string">&quot;a&quot;</span>] = <span class="hljs-number">1</span>,<br>    [<span class="hljs-string">&quot;b&quot;</span>] = <span class="hljs-number">2</span>,<br>    [<span class="hljs-string">&quot;c&quot;</span>] = <span class="hljs-number">3</span><br>&#125;;<br>dict.Add(<span class="hljs-string">&quot;d&quot;</span>, <span class="hljs-number">4</span>);<br>dict.Insert(<span class="hljs-number">0</span>, <span class="hljs-string">&quot;e&quot;</span>, <span class="hljs-number">5</span>);        <span class="hljs-comment">// 支持在特定索引处插入</span><br><br>Console.WriteLine(dict[<span class="hljs-number">0</span>]);    <span class="hljs-comment">// 通过索引访问，输出 5</span><br>Console.WriteLine(dict[<span class="hljs-string">&quot;b&quot;</span>]);  <span class="hljs-comment">// 通过键访问，输出 2</span><br></code></pre></td></tr></table></figure><h3 id="底层实现与权衡"><a class="markdownIt-Anchor" href="#底层实现与权衡"></a> 底层实现与权衡</h3><p>普通的 <code>Dictionary</code> 在刚开始追加插入时其实也是有顺序的，但一旦发生 <code>Remove</code> 操作，它会通过 <code>_freeList</code>（空闲链表）将原位置的值去掉，后续再 <code>Add</code> 元素时会优先填补这些空缺。这就导致常规字典在经历过增删后，遍历顺序会与插入顺序完全脱节。</p><p>而 <code>OrderedDictionary&lt;TKey, TValue&gt;</code> 为了保证任何时候的严格连续和保序，它的底层在维护哈希桶的同时，内部存储键值的数组是始终保持紧凑且按顺序排列的。</p><p>当你从<code>OrderedDictionary</code>删除或插入一个元素时，为了维护内部数组的连续性和顺序，它必须进行数组元素的块移动（Shifting）。这意味着它的删除和特定位置插入操作的时间复杂度退化成了 O(n)。</p><p>因此，<code>OrderedDictionary</code> 非常适合于需要保持严格顺序、高频读取和追加，但极少在中间进行删除的场景（例如解析保序的 JSON 对象、维护 UI 渲染列表的数据源等），但如果你需要在中间频繁地进行删除操作，它的性能表现会受到明显限制。</p><h2 id="frozendictionary"><a class="markdownIt-Anchor" href="#frozendictionary"></a> FrozenDictionary</h2><p>在经历了字典、并发字典和不可变字典的演进后，微服务架构的兴起带来了新的需求：在某些场景下，我们需要一个只读的、线程安全的字典。</p><p>考虑一个业务场景，一个 <code>ASP.NET</code> App 启动时，需要从数据库和配置文件中加载一个带有国际化、路由表什么的巨型字典，这个字典在运行时是不会被修改的，但需要频繁地被访问。</p><p>相对于普通的<code>Dictionary</code>总是要处理潜在的冲突，而<code>FrozenDictionary</code>在设计上就假设了它的内容在创建后是不会被修改的，因此它可以将每个键都放在哈希表中，由于位置可以立即确定，无需要处理碰撞问题，这样就提升了访问性能。</p><p><em>反着想，既然<code>FrozenDictionary</code>需要在一开始最优地处理键值对，那么它的构建过程就会非常慢，因为它需要对所有的键进行哈希计算和位置调整，以确保没有碰撞发生。</em></p><p>那么，<code>FrozenDictionary</code>的核心在于它在<strong>初始化</strong>的时候发生了什么。</p><h3 id="根据键来初筛"><a class="markdownIt-Anchor" href="#根据键来初筛"></a> 根据键来初筛</h3><p>我们可以借助<code>.ToFrozenDictionary()</code>方法来创建一个<code>FrozenDictionary</code>实例。观察源代码的第一个<code>if</code>注释：</p><blockquote><p>Optimize for value types when the default comparer is being used. In such a case, the implementation may use {Equality}Comparer<TKey>.Default.Compare/Equals/GetHashCode directly, with generic specialization enabling the Equals/GetHashCode methods to be devirtualized and possibly inlined.</p></blockquote><p>如果<code>TKey</code>是一个值类型，并且使用了默认的比较器，那么在构建<code>FrozenDictionary</code>时，可以直接使用<code>EqualityComparer&lt;TKey&gt;.Default</code>来进行比较和哈希计算，这样就可以利用泛型的特性来实现方法的去虚化和内联，从而提升性能。</p><p>其中，对于<code>int</code>类型，我们可以直接将键的存储位置和哈希值的存储位置合并，因为<code>int</code>类型的键本身就是一个哈希值，这样就避免了额外的内存开销和访问层级。</p><hr /><p>第二个<code>if</code>注释提到：</p><blockquote><p>Optimize for string keys with the default, Ordinal, or OrdinalIgnoreCase comparers. If the key is a string and the comparer is known to provide ordinal (case-sensitive or case-insensitive) semantics, we can use an implementation that’s able to examine and optimize based on lengths and/or subsequences within those strings.</p></blockquote><p>如果<code>TKey</code>是一个字符串，并且使用了默认的比较器或者是<code>Ordinal</code>、<code>OrdinalIgnoreCase</code>比较器，那么在构建<code>FrozenDictionary</code>时，可以利用字符串的特性来进行优化，比如根据字符串的长度或者子序列来快速定位键的位置，从而提升访问性能。</p><h3 id="构建策略"><a class="markdownIt-Anchor" href="#构建策略"></a> 构建策略</h3><p>整个类藏了很多不同的底层实现类，根据上一个步骤的分析结果，<code>FrozenDictionary</code>会选择不同的构建策略来创建内部的数据结构，以确保在访问时能够达到最佳性能。比如<code>SmallValueTypeComparableFrozenDictionary</code>、<code>Int32FrozenDictionary</code>、<code>ValueTypeDefaultComparerFrozenDictionary</code>等等。</p><p>这些类都是<code>internal</code>的，意味着它们是<code>FrozenDictionary</code>的内部实现细节，外部用户并不需要关心它们的存在，只需要通过<code>FrozenDictionary</code>提供的接口来使用即可。如果你对它们的实现细节感兴趣，可以直接<a href="https://source.dot.net/#System.Collections.Immutable/System/Collections/Frozen/FrozenDictionary.cs,2c211e638443b091,references">查看源代码</a>来了解它们是如何根据不同的键类型和比较器来优化访问性能的。</p><p>如果哪个优化都没有命中，那么就会退回到一个通用的实现（<code>DefaultFrozenDictionary</code> -&gt; <a href="https://source.dot.net/#System.Collections.Immutable/System/Collections/Frozen/KeysAndValuesFrozenDictionary.cs,93d7aeb9a39b49b6"><code>KeysAndValuesFrozenDictionary</code></a>）。</p><h2 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h2><p>从<code>Hashtable</code>到<code>Dictionary</code>的优化，再到<code>ConcurrentDictionary</code>、<code>ImmutableDictionary</code>、<code>OrderedDictionary</code>和<code>FrozenDictionary</code>对不同场景的适配。随着 .NET 10 的到来，JIT 介入、逃逸分析、零拷贝 Span 等技术的引入，我们应当继续关注这些数据结构在性能和内存方面的优化，选择最适合我们业务场景的实现来使用。</p>]]>
    </content>
    <id>https://ziling.moe/2026/dotnet-data-structure-key-value-pair/</id>
    <link href="https://ziling.moe/2026/dotnet-data-structure-key-value-pair/"/>
    <published>2026-02-23T16:01:00.000Z</published>
    <summary>键值对与哈希集合数据结构的演进。</summary>
    <title>.NET 数据结构大杂烩：键值对</title>
    <updated>2026-02-23T16:01:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term="Sylinko" scheme="https://ziling.moe/categories/Sylinko/"/>
    <category term="Sylinko" scheme="https://ziling.moe/tags/Sylinko/"/>
    <category term="Everywhere" scheme="https://ziling.moe/tags/Everywhere/"/>
    <category term="AI" scheme="https://ziling.moe/tags/AI/"/>
    <category term="Agent" scheme="https://ziling.moe/tags/Agent/"/>
    <content>
      <![CDATA[<blockquote><p>Of all things the measure is Man, of the things that are, that they are, and of the things that are not, that they are not.</p></blockquote><h2 id="引言"><a class="markdownIt-Anchor" href="#引言"></a> 引言</h2><p>上周，我与一位 VC 线上商讨融资事宜的时候，对方提到他们非常看好 Agentic AI (AGI) 的未来，用户不再需要直接和其他软件交互，而是由 AI 代理一切。与此同时，诸如 Glass、MineContext 这类产品也在积极推动这个概念，让 AI 全时段准备、主动介入用户的工作和生活。</p><p>AI 作为工具的时代似乎正在走向终结，人们心中完美的管家似乎正在成型，尽管这一切都是充满安全漏洞的概率游戏也值得。代理了你的屏幕和文件，持续揣测行为意图，甚至是上传隐私数据。作为用户的我们，真的准备好迎接并接受与 AI 主客关系颠倒的未来了吗？</p><p>从这个观点来看，我们的被动式 AI，即用户按下快捷键后 AI 才介入反倒成了技术上的保守，甚至是在 AGI 风头正盛的时候显得有些过时。但所谓的 Age of Scaling —— 认为只要堆砌算力和数据，力大砖飞，就能涌现出通用的智能 —— 已经面临越来越多的挑战。</p><blockquote><p>在工场手工业中，工人利用工具；在工厂中，工人服侍机器。</p></blockquote><p>人的自由意志经历了文艺复兴、启蒙运动等历史阶段的洗礼，才逐渐确立了现代意义上的主体地位。从工业革命、信息革命到现在，技术的进步不断挑战着人的主体性。而 AGI 的兴起，尤其是当它试图以更懂你的名义全面介入生活时，似乎又将我们推向了一个新的十字路口。</p><p>不得不承认的是，我能通过 Copilot 感受到编程效率的提升，向 ChatGPT、Gemini 寻求建议也确实能节省不少时间。赛博外骨骼的出现，让我在体力劳动中获得了前所未有的便利。</p><p>但 AGI 不一样，它与人的根本矛盾在于控制权而非效率。大厂们恨不得将他们的 AI 塞到人们生活的每一秒中，意图创造一个“无缝的体验”。但这种主动似乎逐渐失去了边界感，同时带来了焦虑和不适。</p><p>我们选择做被动式 AI，是因为我们仍将 AI 视为工具，而非主宰。我们相信，技术应当服务于人，而非取代人。高质量的信息由人产生，高效的执行由 AI 辅助。用户应当始终掌握控制权。</p><p>我们没有在技术上选择保守，而是坚守对用户自由意志的尊重与控制权的回归。</p><p>本文，我不想谈论太多 AGI 的幻想与未来，而是从隐私法规、算力成本和心理学的约束下，探讨这一热门的被资本追捧的概念。</p><h2 id="隐私"><a class="markdownIt-Anchor" href="#隐私"></a> 隐私</h2><p>人们谈及对隐私的消极态度时，往往都来源于大数据在各个软件的围追堵截带来的无力感。个人、企业与政府之间的博弈也让用户感到力不从心。</p><p>AGI 这种系统性的失控，让我担心起福柯的全景敞视主义：全知全能的监视者通过无处不在的监视让被监视者自觉地规训自己。尽管这句话来自于对法律和权力的批判，但在 AGI 的语境下，它同样适用。当一个 AI 宣称它能够“一直看着你”的同时“服务你”必然会带来被凝视感。</p><p>潜意识里的心理压迫会时刻影响用户的状态，当你进行私密聊天、工作摸鱼、处理银行信息时，总有个智能体在旁边看着你。审视的目光也会逐渐压制用户的创造时刻，那个最放松最自由的时刻。</p><p>不被注视的自由才是隐私安全对人的意义。</p><h3 id="翻车的-recall"><a class="markdownIt-Anchor" href="#翻车的-recall"></a> 翻车的 Recall</h3><p>去年，微软推出了 Windows Recall 功能，试图通过每隔几秒截图来帮用户回溯记忆。但在顶级的法务团队手中依然没能活过公测。本地数据库被攻破后，用户的隐私信息被泄露。最后舆论哗然，监管介入。</p><p>这算是我印象里比较早的 AGI 雏形了，但即使是微软也难以打消用户对隐私的担忧。大厂怎么博弈我们管不着，也管不到。我们只知道以一个小初创团队的身份，想要说服用户把隐私交给一个持续监控的 AI，是多么困难的一件事。</p><h3 id="全球合规"><a class="markdownIt-Anchor" href="#全球合规"></a> 全球合规</h3><p>AGI 自然被授予获取大量数据源的权限（比如全盘文件或屏幕），同时也有记忆，那我们看看各地区的隐私法规如何。</p><p>开发者都知道在隐私方面，欧盟是个难啃的骨头。它对于数据保护的法案写在通用数据保护条例（GDPR）中，其中的<a href="https://ico.org.uk/for-organisations/uk-gdpr-guidance-and-resources/data-protection-principles/a-guide-to-the-data-protection-principles/data-minimisation/">数据最小化原则（Data Minimization Principle）</a>明确规定，收集的数据应当限于实现特定目的所必需的最少量。</p><blockquote><p>您必须确保您处理的个人数据是：<br />足够 - 能够充分实现你所期望的目标；<br />相关 - 与该目的有合理的联系；并且<br />仅限于必要 - 你不需要的就不要持有。</p></blockquote><p>然后是北美，最近在编写隐私政策的时候注意到加州对隐私的保护也不简单。虽然不像欧盟那样对收集这一块卡那么死，但加州消费者隐私法案（CCPA）也要求企业在收集个人信息前必须明确告知用户，并提供选择退出的权利。这意味着，如果 AGI 需要持续监控用户的行为，必须确保用户完全知情并同意这种监控方式。同时当地的集体诉讼文化也会让企业在发生数据泄露时面临巨额赔偿。</p><p>最后是中国，迷幻的隐私环境让人捉摸不透，那还是谈谈熟悉的现状。从用户角度来看，尽管它可能不在意隐私，但一些行业（例如银行、广告、医疗等）的服务或软件对用户数据保护更加严格。但最近刚好就有个例子：</p><h3 id="静默的数据泄露"><a class="markdownIt-Anchor" href="#静默的数据泄露"></a> 静默的数据泄露</h3><p>这个月，豆包手机问世，第一天惊艳、第二天一机难求、第三天发现微信用不了。实测23款主流 App（包括淘宝、美团、支付宝等）无法登录；微信、高德地图、大麦网3款 App 可以手动登录但无法通过 AI 操作。</p><p>不同于传统 App 仅在用户交互时获取权限和相关信息，AGI 的行为内含的授权更广泛。在分析用户行为特征时，仅仅是为了执行一个操作，用户的画像都逐渐清晰了。不假思索地将自己的数据交给一个可疑的代理人，让代理人和各种 App 交互，转手了隐私授权的直观感知，这本身就是一种隐患。</p><p>在这个环境下，企业的利益驱动的作用非常明显。当豆包手机能够精准关闭广告时，企业的广告投放效果大打折扣，直接影响了它们的收入；买机票的时候特别提醒用户是否需要加购保险。这些都会或多或少的影响企业的盈利模式。</p><p>同时豆包也会在点外卖的时候加入自己的意见，在凑单的时候自动加入商品。用户体验的提升背后，是企业对用户行为的深度干预。</p><p>这种商业利益的博弈，最终牺牲的往往是用户的体验。当 AI 为了“主动”而不得不扫描每一个 App 的界面布局时，它不仅触犯了互联网公司的护城河，更让用户成为了巨头战争中的炮灰。</p><h2 id="数据安全"><a class="markdownIt-Anchor" href="#数据安全"></a> 数据安全</h2><p>不仅数据泄露是隐私安全的隐患，AGI 对数据本身的威胁也不容忽视。当人将自主权交给它时，数据的完整性和安全性也面临着新的挑战。</p><p>我个人的态度是在使用这类软件时，用别怕，怕别用。</p><h3 id="为-ai-的幻觉买单"><a class="markdownIt-Anchor" href="#为-ai-的幻觉买单"></a> 为 AI 的幻觉买单</h3><p>哪怕是 Coding Agent，诸如 Claude Code 出现的幻觉或是一点小小失误也足以对你的代码库造成不可逆的破坏。</p><div class="tag-plugin grid"  style="grid-template-columns: repeat(2, 1fr);"><div class="cell" style="">    <div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1079/880;"><img class="lazy" src="/images/2025/sylinko-everywhere-criticize-agentic-ai/claude-code.webp" data-src="/images/2025/sylinko-everywhere-criticize-agentic-ai/claude-code.webp" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div>    </div>    <div class="cell" style="">    <div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1037/1126;"><img class="lazy" src="/images/2025/sylinko-everywhere-criticize-agentic-ai/antigravity.webp" data-src="/images/2025/sylinko-everywhere-criticize-agentic-ai/antigravity.webp" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div>    </div>    </div><p>倒不是说这些 Scoped Agent 本身不靠谱，而是它作为一个工具，使用它的人应当学会承受风险。</p><p>当然有用 Git 的小白不小心 discard 了自己的成果，这些工具也是一样的，用户最好做好风险隔离或是限制的准备，使用备份、沙箱环境都是可以的。</p><h3 id="被代理的自主权"><a class="markdownIt-Anchor" href="#被代理的自主权"></a> 被代理的自主权</h3><p>Claude Cowork 刚出现那会把我吓了一跳，不只是它的出现似乎象征着 AI 大厂们对 AGI 的野心，更在于它的风险。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1324/1216;width:450px;"><img class="lazy" src="/images/2025/sylinko-everywhere-criticize-agentic-ai/cowork.webp" data-src="/images/2025/sylinko-everywhere-criticize-agentic-ai/cowork.webp" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div><p>三次改名的 ClawdBot -&gt; MoltBot -&gt; OpenClaw 更是经典案例，至少上面的代码库事故可以通过版本控制来挽回。那么当人们授予其更多权限并介入到更广泛的场景时，数据的安全性又该如何保障？</p><p>最近更是爆出了<a href="https://x.com/theonejvo/status/2015401219746128322">安全漏洞</a>，作为一个高权限管家，它可以运行 Shell 命令并读写文件，当项目本身出现安全问题时，拿到了 Chat 的界面就等于能全范围攻击了。</p><h2 id="止步于理想的体验"><a class="markdownIt-Anchor" href="#止步于理想的体验"></a> 止步于理想的体验</h2><p>如果说隐私和合规是悬在它们头顶上的达摩克利斯之剑，那么可用性就是绊脚石，AGI 的干涉往往演变成了一种粗暴的打扰。</p><h3 id="意图识别的妄想"><a class="markdownIt-Anchor" href="#意图识别的妄想"></a> 意图识别的妄想</h3><p>AGI 的无缝体验建立在一个假设：AI 能够精准预测用户下一秒想干什么，或是需要什么帮助。</p><p>但在屏幕感知这个场景下，猜测意图的难度远高于执行指令。目前的 LLM 虽然在文本生成上表现出色，但在理解屏幕上下文的潜台词时，依然经常产生幻觉。</p><p>想象一下，当你正在专注地阅读一份竞品的财报时，AGI 认为你可能需要它生成五彩斑斓的饼图。几个小小的建议就完全有能力打断一个人的思考，从而暗中跟着 AI 的节奏走。</p><p>错误的主动，比不做更可怕。它不仅没有提升效率，反而成为了最大的噪音。</p><h3 id="生产力的天敌"><a class="markdownIt-Anchor" href="#生产力的天敌"></a> 生产力的天敌</h3><p>心理学家米哈里·契克森米哈赖 <em>Mihaly Csikszentmihalyi</em> 提出的心流 <em>Flow</em> 理论认为：</p><blockquote><p>心流是一种专注或完全沉浸在当下活动和事情的状态。沉浸在心流状态的人会感受到涅磐般的快乐。心流时的内在动机的达到最佳，让人完全沉浸在他们正在做的事情中。每个人都有过心流体验，会在感到全神贯注、充满成就感的同时，忽略世俗的需求（时间、食物、自我等）。</p></blockquote><p>程序员在写代码、作家在写作、设计师在绘图时，最怕的就是被打断。而 AGI 就像是 20 年前微软 Office 里的回形针（Clippy），像一个没有眼力见的实习生，每隔五分钟就跳出来问你这个要不要我帮你弄？</p><p>对于专业用户而言，不可控的惊喜，往往意味着惊吓。用户宁愿多按一个快捷键来换取 100% 确定的响应，也不愿被一个只有 80% 准确率的弹窗打断思路。</p><h3 id="极低的信噪比"><a class="markdownIt-Anchor" href="#极低的信噪比"></a> 极低的信噪比</h3><p>我们的屏幕上，大部分都是对你的注意力无关紧要的信息。</p><p>从休闲的场景来看：发呆时的桌面、循环播放的歌词、微信群的一个个回复、摸鱼时刷的短视频，保持全能和响应的 AGI 就会时刻对他们进行分析和理解。但厂商真的如此慷慨，时刻提供“值得被称为是生产力”的模型吗？至少在目前的成本控制下，答案是否定的。</p><p>这种极低的信噪比导致了严重的浪费。为了维持持续监控的成本，竞品往往被迫使用参数量较小、智商较低的模型来处理数据。最终花了大价钱，养了一个并不怎么聪明的助手。</p><p>从专业的场景来看：在阅读一篇论文时，专注的点始终在眼球盯着的地方，而非整个屏幕，但 AGI 却不得不对整个屏幕进行分析。大量的无关信息（比如侧边栏、工具栏、背景图等）都需要被处理和过滤。除非哪天接入眼动仪了，否则 AGI 只能对整个屏幕进行盲目地理解。</p><h2 id="人的主体性"><a class="markdownIt-Anchor" href="#人的主体性"></a> 人的主体性</h2><p><s><em>到了我最喜欢的键哲环节了。</em></s></p><h3 id="意志的异化"><a class="markdownIt-Anchor" href="#意志的异化"></a> 意志的异化</h3><p>在工业生产中，工人出卖了劳动力；而现在，用户将出卖决策权。</p><p>在这里，意志的自由与效率被放到天平上，用户通过让渡了执行权给 AGI，以在表面上获得更高的生产力。</p><p>不同于工业时代的机器逐步取代体力劳动，更为深刻的是，AGI 在向神经系统动刀。由于 AI 的运作往往是黑箱，用户无法理解 AGI 做出某一决策背后的具体参数，导致其从行动的主体退化为算法运行的被动接受者。</p><p>与此同时，算力和数据成为了这个时代的生产资料，下游的技术路线被迫跟随大厂的节奏和基础设施。与大数据时代不同，用户的数据不仅被收割，更是成为反过来干预用户行为的原材料。</p><p>AI 是人思考的延申？还是替代？这个问题的出现本身就成问题。</p><h3 id="认知萎缩与摩擦"><a class="markdownIt-Anchor" href="#认知萎缩与摩擦"></a> 认知萎缩与摩擦</h3><blockquote><p>几乎在所有情况下，人类都以一种平凡而又更深入的方式参与到世界中，为了达成某种目标而承担各种任务。以锤子为例：它触手可及；我们使用它时无需思考。<br />—— 海德格尔</p></blockquote><p><em>我还没赋予其其他含义，只是个联想到的现象。</em></p><p>AI 带来认知，AGI 则是认知工具，而上手的认知工具变得自动化，更容易带来大脑可塑性的丧失。</p><p>肌肉少了活动会萎缩，思维也一样。长期将认知任务外包给 AI 会导致批判性思维、记忆与判断力的衰退。</p><p>总之，AGI 最好是负责的，发现用户的指令模糊、存在漏洞或是涉及风险时，应当顽固（Obstinate）一点，变成一个更显眼对象，反问用户。心流对人来说是很重要的状态，但是陷入无意识的流不是，唤醒主体性，对技术保持审视和纠正的能力，才是我们应当追求的目标。</p><h3 id="人本主义"><a class="markdownIt-Anchor" href="#人本主义"></a> 人本主义</h3><p>以数据为中心，如果 AI 能更高效地处理数据，人类就应当让位吗？</p><p>Effective Acceleration <em>(e/acc, 有效加速主义)</em> 认为宇宙的根本目的是通过热力学过程最大化熵增，而 AI 则是实现这一目标的工具。它不在乎智能的生物学基础，而是将后人类主义视为超越生物学限制的手段。</p><p>作为一个人本主义者，我始终认为技术应按照人类的价值观和需求来设计和使用。大洋彼岸的精英主义倾向，实质上将造成技术垄断在多元价值观上的乌云，我们应当让技术更民主、更包容。</p><p>AI 对人的每次反馈，在微观上体现了人的主体性，但这不够。人是万物的尺度，提升用户对系统的信任和控制权，在每个节点，而非一味追求无缝和自动化，才是我们应当坚持的方向。</p><h3 id="时代与人"><a class="markdownIt-Anchor" href="#时代与人"></a> 时代与人</h3><p>人应当保持对生产资料的掌控，代行的数字意志应当始终在自己的监控之下，自己的数据也应当在本地保存；AI 应当作为认知外骨骼存在，始终处于待命状态，而非主动干预；保持对 AI 的怀疑和审视和对社会造成的影响，维护人的主体性。</p><p>AI 与人的未来不应当陷入后人类的浪潮，在算法的嘈杂声中，我们应当发出属于人类意志的声音。</p><p>每个人都是行动的发起者，人赋予了意义，人是万物的尺度。</p>]]>
    </content>
    <id>https://ziling.moe/2025/sylinko-everywhere-criticize-agentic-ai/</id>
    <link href="https://ziling.moe/2025/sylinko-everywhere-criticize-agentic-ai/"/>
    <published>2025-12-10T14:40:00.000Z</published>
    <summary>人是万物的尺度。</summary>
    <title>VC 眼中的终局，可能是用户的地狱：论桌面 AI 的边界</title>
    <updated>2025-12-10T14:40:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term="Academics" scheme="https://ziling.moe/categories/Academics/"/>
    <category term="UBC" scheme="https://ziling.moe/tags/UBC/"/>
    <category term="计算机科学" scheme="https://ziling.moe/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    <category term="教程" scheme="https://ziling.moe/tags/%E6%95%99%E7%A8%8B/"/>
    <content>
      <![CDATA[<p>在一个列表中寻找元素是一个很简单的需求，但在实际生活中，它的数据量往往非常庞大。这时候我们就需要考虑<strong>性能</strong>这一要求，使用更高效的数据结构来提高查找效率。</p><p>这个新的自我引用数据结果就是<emp>二叉搜索树</emp>。 <em>(Binary Search Tree, BST)</em></p><p>我们将围绕 BST 设计数据定义和函数，所用的知识和往常无异，当然，递归也是会被经常用到的。</p><h2 id="学习目标"><a class="markdownIt-Anchor" href="#学习目标"></a> 学习目标</h2><ul><li>能够非正式地推理搜索数据所需的时间</li><li>能够识别适合用二叉搜索树表示的问题域 <em>(problem domain)</em> 信息</li><li>能够检查给定树是否符合二叉搜索树的不变式</li><li>能够运用设计方法进行二叉搜索树的设计</li></ul><mark class="tag-plugin colorful mark" color="warning">以下内容涉及到的edX链接均不保证可访问性</mark><h2 id="list-abbreviations"><a class="markdownIt-Anchor" href="#list-abbreviations"></a> List Abbreviations</h2><p>在研究 BST 之前，介绍一个定义列表的简便方法，比连着写一串<code>cons</code>更好看。比如说，对于<code>(cons &quot;a&quot; (cons &quot;b&quot; (cons &quot;c&quot; empty)))</code>，我们可以把它写成<code>(list &quot;a&quot; &quot;b&quot; &quot;c&quot;)</code>。</p><details class="tag-plugin colorful folding" color="blue" open><summary><p>Choose Language</p></summary><div class="body"><p>记得在 DrRacket 的左下方，将<code>Beginning Student</code>切换为<code>Beginning Student with List Abbreviations</code>。</p> <div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:428/154;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-binary-search-trees/languages.webp" data-src="/images/2025/academics-ubc-cpsc-110-binary-search-trees/languages.webp" alt="选择语言" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">选择语言</span></div></div> <p>切换后，运行刚刚的<code>cons</code>和<code>list</code>你会得到相同的结果：</p> <figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-string">&quot;a&quot;</span> <span class="hljs-string">&quot;b&quot;</span> <span class="hljs-string">&quot;c&quot;</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-string">&quot;a&quot;</span> <span class="hljs-string">&quot;b&quot;</span> <span class="hljs-string">&quot;c&quot;</span>)<br></code></pre></td></tr></table></figure> </div></details><p>那我们之前为什么要费劲写这么多<code>cons</code>？只是为了介绍递归的概念不得不做的事。在熟悉它们后，自然就可以切换到更简单的写法了。</p><p><code>list</code>表达式可以传入多个元素，而不仅仅是两个。同时，构建时里面的表达式也会被计算，比如：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">list</span></span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">3</span> <span class="hljs-number">4</span>) (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">5</span> <span class="hljs-number">6</span>))<br>&gt; (<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-number">3</span> <span class="hljs-number">7</span> <span class="hljs-number">11</span>)<br></code></pre></td></tr></table></figure><details class="tag-plugin colorful folding" color="orange" open><summary><p>行为不同</p></summary><div class="body"><p>和<code>cons</code>不同，它的拼接过程本质不一样。<code>cons</code>在将元素和列表合并的时候，会将元素<strong>插入</strong>列表的首位。而<code>list</code>会将列表视为一个整体，将列表放到元素之后：</p> <figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;a&quot;</span> (<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-string">&quot;b&quot;</span> <span class="hljs-string">&quot;c&quot;</span>))<br>&gt; (<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-string">&quot;a&quot;</span> <span class="hljs-string">&quot;b&quot;</span> <span class="hljs-string">&quot;c&quot;</span>)<br><br>(<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-string">&quot;a&quot;</span> (<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-string">&quot;b&quot;</span> <span class="hljs-string">&quot;c&quot;</span>))<br>&gt; (<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-string">&quot;a&quot;</span> (<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-string">&quot;b&quot;</span> <span class="hljs-string">&quot;c&quot;</span>))<br></code></pre></td></tr></table></figure> <p>如果想让<code>list</code>达到相同效果，可以使用<code>append</code>表达式：</p> <figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">append</span></span> (<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-string">&quot;b&quot;</span> <span class="hljs-string">&quot;c&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-string">&quot;d&quot;</span> <span class="hljs-string">&quot;e&quot;</span> <span class="hljs-string">&quot;f&quot;</span>))<br>&gt; (<span class="hljs-name"><span class="hljs-built_in">list</span></span> <span class="hljs-string">&quot;b&quot;</span> <span class="hljs-string">&quot;c&quot;</span> <span class="hljs-string">&quot;d&quot;</span> <span class="hljs-string">&quot;e&quot;</span> <span class="hljs-string">&quot;f&quot;</span>)<br></code></pre></td></tr></table></figure> </div></details><h2 id="list-of-account-binary-search-trees"><a class="markdownIt-Anchor" href="#list-of-account-binary-search-trees"></a> List of Account &amp; Binary Search Trees</h2><p>在面对大量用户信息时，使用列表来存储和管理这些信息是一个常见的做法。我们可以使用二叉搜索树来优化对这些用户信息的查找和管理。<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/lookup-in-list-starter.rkt">下载来自edX的 lookup-in-list-starter.rkt 文件</a>开始。</p><p>观察代码文件的内容，<code>account</code>含有字段<code>num</code>和<code>name</code>，所以对于一个用户列表来说，它可以是<code>(list (make-account 1 &quot;abc&quot;) (make-account 4 &quot;dcj&quot;) (make-account 3 &quot;ilk&quot;) (make-account 7 &quot;ruf&quot;))</code></p><p>虽然这里只有4个用户，但当用户数量非常非常多（几十上百万个）时，我们该如何根据用户的<code>num</code>来查找它的名字呢？以往的遍历耗时将会很长。（特别是当要查找的用户好巧不巧在末尾）</p><p>当然，运气好的话在一开始找到也是可能的，所以平均下来我们需要查找<code>n/2</code>个用户，这对于很大的用户量是完全不满足的。</p><hr /><p>考虑一个用户列表（为了简化表达，诸如<code>(make-account 10 &quot;why&quot;)</code>会被简化为<code>10:why</code>）：<code>(list 1:abc 3:ilk 4:dcj 7:ruf 10:why 14:olp 27:wit 42:ily 50:dug)</code>。</p><p>首先，它是个有序列表（无序也得将其变成有序的），然后从中间元素（<code>10:why</code>）拆开，左边是<code>(list 1:abc 3:ilk 4:dcj 7:ruf)</code>，右边是<code>(list 14:olp 27:wit 42:ily 50:dug)</code>。保证在中间元素左边列表的<code>num</code>都比它小，右边列表的<code>num</code>都比它大。</p><p>以此类推，对之后的每个左侧和右侧列表执行这样的操作，就能得到一个树状图：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1658/626;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-binary-search-trees/split-tree.webp" data-src="/images/2025/academics-ubc-cpsc-110-binary-search-trees/split-tree.webp" alt="二叉搜索树" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">二叉搜索树</span></div></div><details class="tag-plugin colorful folding" color="gray" open><summary><p>BST</p></summary><div class="body"><p>二叉搜索树属于<emp>二叉树</emp> <em>(Binary Tree)</em>，它是一种特殊的二叉树，具有以下性质：</p> <ol> <li>每个节点都有一个值（key），并且每个节点的值都大于其左子树中所有节点的值。</li> <li>每个节点的值都小于其右子树中所有节点的值。</li> <li>左右子树也都是二叉搜索树。</li> </ol> <p>这种结构使得在平均情况下，极大地提高了查找、插入和删除操作的效率。</p> </div></details><p>回到这个 BST 本身，假如我们要寻找<code>num</code>为<code>14</code>的用户，我们可以从根节点<code>10:why</code>开始：</p><ul><li>发现<code>14</code>比<code>10</code>大，所以应该往右子树走，进入到<code>42:ily</code></li><li>发现<code>14</code>比<code>42</code>小，所以应该往<code>42:ily</code>的左子树走，进入到<code>27:wit</code></li><li>发现<code>14</code>比<code>27</code>小，所以应该往<code>27:wit</code>的左子树走，进入到<code>14:olp</code></li><li>发现<code>14:olp</code>刚好是我们需要的数据</li></ul><p>这样，我们就将平均搜索次数从<code>n/2</code>降低到了<code>log n</code>。</p><h2 id="a-data-definition-for-bsts"><a class="markdownIt-Anchor" href="#a-data-definition-for-bsts"></a> A Data Definition for BSTs</h2><p>了解 BST 的机制和原理之后，这一节要求我们实现一个 BST 数据结构。<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/bst-dd-starter.rkt">下载来自 edX 的 bst-dd-starter.rkt 文件</a>。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1038/624;width:500px;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-binary-search-trees/bst-dd-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-binary-search-trees/bst-dd-starter.webp" alt="bst-dd-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">bst-dd-starter.rkt</span></div></div><p>观察注释块，它提供了一个 BST 的例子。实际上这个数据结构是由节点组成的，每个节点包含<strong>一个键值对</strong>和指向左右子树的<strong>引用</strong>，所以其本质上就是个复合数据类型：</p><ul><li>键 <em>(Key)</em>：用于查询，在这里是<code>num</code>。</li><li>值 <em>(Value)</em>：与键对应的用户信息，在这里是<code>name</code>。</li><li>左子树 <em>(Left Subtree)</em>：指向当前节点左侧的子树，包含所有小于当前节点键的值。</li><li>右子树 <em>(Right Subtree)</em>：指向当前节点右侧的子树，包含所有大于当前节点键的值。</li></ul><p>姑且将这个复合数据类型称为<code>node</code>，它包含<code>key</code>、<code>val</code>、<code>l</code>和<code>r</code>。但并不是每个节点都有左右子树，因此我们需要对<code>l</code>和<code>r</code>做一些特殊处理。</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">define-struct</span> node (<span class="hljs-name">key</span> val l r))<br></code></pre></td></tr></table></figure><p>同时需要定义组成这些<code>node</code>的树，我们可以将其定义为<code>BST</code>，它可以是一个<code>node</code>，也可以是<code>false</code>（空树）。这样就能处理子树可能为空的问题。同时存在一个约定，左右子树的值都要比父节点小或大。</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; BST (Binary Search Tree) is one of:</span><br><span class="hljs-comment">;;   - false</span><br><span class="hljs-comment">;;   - (make-node Integer String BST BST)</span><br><span class="hljs-comment">;; interp. false means no BST, or empty BST</span><br><span class="hljs-comment">;;         key is the node key</span><br><span class="hljs-comment">;;         val is the node value</span><br><span class="hljs-comment">;;         l and r are left and right subtrees</span><br><span class="hljs-comment">;; INVARIANT: for a given node:</span><br><span class="hljs-comment">;;     key is &gt; all keys in its l(eft) child</span><br><span class="hljs-comment">;;     key is &lt; all keys in its r(ight) child</span><br><span class="hljs-comment">;;     the same key never appears twice in the tree</span><br></code></pre></td></tr></table></figure><p>它的例子如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> BST0 false)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> BST1 (<span class="hljs-name">make-node</span> <span class="hljs-number">1</span> <span class="hljs-string">&quot;abc&quot;</span> false false))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> BST4 (<span class="hljs-name">make-node</span> <span class="hljs-number">4</span> <span class="hljs-string">&quot;dcj&quot;</span> false (<span class="hljs-name">make-node</span> <span class="hljs-number">7</span> <span class="hljs-string">&quot;ruf&quot;</span> false false)))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> BST3 (<span class="hljs-name">make-node</span> <span class="hljs-number">3</span> <span class="hljs-string">&quot;ilk&quot;</span> BST1 BST4))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> BST42 <br>  (<span class="hljs-name">make-node</span> <span class="hljs-number">42</span> <span class="hljs-string">&quot;ily&quot;</span><br>             (<span class="hljs-name">make-node</span> <span class="hljs-number">27</span> <span class="hljs-string">&quot;wit&quot;</span> (<span class="hljs-name">make-node</span> <span class="hljs-number">14</span> <span class="hljs-string">&quot;olp&quot;</span> false false) false)<br>             (<span class="hljs-name">make-node</span> <span class="hljs-number">50</span> <span class="hljs-string">&quot;dug&quot;</span> false false)))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> BST10<br>  (<span class="hljs-name">make-node</span> <span class="hljs-number">10</span> <span class="hljs-string">&quot;why&quot;</span> BST3 BST42))<br></code></pre></td></tr></table></figure><p>考虑到 BST 的函数模板可能需要处理空树或是在有节点的情况下四个字段的信息。需要注意的是，<code>node</code>的左右子树需要被传入回函数进行递归：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs scheme">#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-bst</span> t)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">false?</span> t) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">node-key</span> t)    <span class="hljs-comment">;Integer</span><br>              (<span class="hljs-name">node-val</span> t)    <span class="hljs-comment">;String</span><br>              (<span class="hljs-name">fn-for-bst</span> (<span class="hljs-name">node-l</span> t))<br>              (<span class="hljs-name">fn-for-bst</span> (<span class="hljs-name">node-r</span> t)))]))<br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;  - one of: 2 cases</span><br><span class="hljs-comment">;;  - atomic-distinct: false</span><br><span class="hljs-comment">;;  - compound: (make-node Integer String BST BST)</span><br><span class="hljs-comment">;;  - self reference: (node-l t) has type BST</span><br><span class="hljs-comment">;;  - self reference: (node-r t) has type BST</span><br></code></pre></td></tr></table></figure><p>至此，BST 的数据定义完成了。</p><h2 id="lookup-in-bsts"><a class="markdownIt-Anchor" href="#lookup-in-bsts"></a> Lookup in BSTs</h2><p>如果你的进度中断了，可以<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/lookup-in-bst-starter.rkt">下载来自edX的 lookup-in-bst-starter.rkt 文件</a>开始。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1417/644;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-binary-search-trees/lookup-in-bst-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-binary-search-trees/lookup-in-bst-starter.webp" alt="lookup-in-bst-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">lookup-in-bst-starter.rkt</span></div></div><p>本节会实现一个用于在 BST 中查询信息的函数，通过键找到对应的值。当然，并不是所有的键都有对应的值，因此我们需要处理查找失败的情况。</p><p>函数的签名是：<code>;; BST Natural -&gt; String or false</code>，目的是<code>;; Try to find node with given key, if found produce value; if not found produce false.</code></p><p>其测试需要照顾到返回值或者<code>false</code>的情况：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">lookup-key</span> BST0  <span class="hljs-number">99</span>) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">lookup-key</span> BST1   <span class="hljs-number">1</span>) <span class="hljs-string">&quot;abc&quot;</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">lookup-key</span> BST1   <span class="hljs-number">0</span>) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">lookup-key</span> BST1  <span class="hljs-number">99</span>) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">lookup-key</span> BST10  <span class="hljs-number">1</span>) <span class="hljs-string">&quot;abc&quot;</span>)  <span class="hljs-comment">;L L</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">lookup-key</span> BST10  <span class="hljs-number">4</span>) <span class="hljs-string">&quot;dcj&quot;</span>)  <span class="hljs-comment">;L R</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">lookup-key</span> BST10 <span class="hljs-number">27</span>) <span class="hljs-string">&quot;wit&quot;</span>)  <span class="hljs-comment">;R L</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">lookup-key</span> BST10 <span class="hljs-number">50</span>) <span class="hljs-string">&quot;dug&quot;</span>)  <span class="hljs-comment">;R R</span><br></code></pre></td></tr></table></figure><p>将函数模板拷过来，改名。模板本身接受的是树，但我们同时需要查询值，所以需要加一个<code>k</code>表示键：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">lookup-key</span> t k)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">false?</span> t) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">...</span></span> k<br>              (<span class="hljs-name">node-key</span> t)    <span class="hljs-comment">;Integer</span><br>              (<span class="hljs-name">node-val</span> t)    <span class="hljs-comment">;String</span><br>              (<span class="hljs-name">fn-for-bst</span> (<span class="hljs-name">node-l</span> t) (<span class="hljs-name"><span class="hljs-built_in">...</span></span> k))<br>              (<span class="hljs-name">fn-for-bst</span> (<span class="hljs-name">node-r</span> t) (<span class="hljs-name"><span class="hljs-built_in">...</span></span> k)))]))<br></code></pre></td></tr></table></figure><p>对于空树的情况，或者说是查询到底了也没找着，自然就返回<code>false</code>。否则就进入到一个<code>cond</code>判断，如果当前节点的<code>key</code>等于查询的<code>k</code>，就返回当前节点的<code>val</code>。如果小于，就递归到左子树继续查找；如果大于，就递归到右子树继续查找：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">lookup-key</span> t k)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">false?</span> t) false]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">=</span></span> k (<span class="hljs-name">node-key</span> t)) (<span class="hljs-name">node-val</span> t)]<br>               [(<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> k (<span class="hljs-name">node-key</span> t)) (<span class="hljs-name">lookup-key</span> (<span class="hljs-name">node-l</span> t) k)]<br>               [(<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> k (<span class="hljs-name">node-key</span> t)) (<span class="hljs-name">lookup-key</span> (<span class="hljs-name">node-r</span> t) k)]<br>               [<span class="hljs-name"><span class="hljs-built_in">else</span></span> (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)])]))<br></code></pre></td></tr></table></figure><p><code>else</code>的存在是为了程序的健壮性，尽管在正常情况下不应该到达这个分支。</p><p>至此8个测试都通过了，我们成功实现了这个函数。</p><h2 id="rendering-bsts"><a class="markdownIt-Anchor" href="#rendering-bsts"></a> Rendering BSTs</h2><p>本节将会尝试把二叉搜索树渲染出来，在此之前，<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/render-bst-starter.rkt">下载来自 edX 的 render-bst-starter.rkt 文件</a>。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:868/199;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-binary-search-trees/render-bst-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-binary-search-trees/render-bst-starter.webp" alt="render-bst-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">render-bst-starter.rkt</span></div></div><p>这个文件已经把节点和树定义好了，留给我们一个问题：设计一个函数，接受一棵 BST，返回它的简易渲染图像。</p><p>BST 的图像应该很熟悉了，就是多个节点之间用线连接。那我们该怎么做呢？</p><p>我们可以效仿 BST 的搜索过程，也从每个节点出发寻找它的左右子树，将左右子树的渲染留给下一次递归。左右子树之间是有间隔的，它们也和父节点之间有一定的距离：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1000/600;width:500px;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-binary-search-trees/render-bst-starter-analysis.webp" data-src="/images/2025/academics-ubc-cpsc-110-binary-search-trees/render-bst-starter-analysis.webp" alt="示意图" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">示意图</span></div></div><p>至此，我们需要定义一些常量：</p><ul><li>字体大小和颜色</li><li>键值对之间的分隔符 <em>(图里是<code>:</code>)</em></li><li>节点与左右子树之间的垂直间隔距离</li><li>左右子树之间的水平间隔距离</li><li>空树的占位图 <em>(很容易被忽视)</em></li></ul><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Constants:</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> TEXT-SIZE <span class="hljs-number">14</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> TEXT-COLOR <span class="hljs-string">&quot;black&quot;</span>)<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> KEY-VAL-SEPARATOR <span class="hljs-string">&quot;:&quot;</span>)<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> VSPACE (<span class="hljs-name">rectangle</span> <span class="hljs-number">1</span>  <span class="hljs-number">10</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;white&quot;</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> HSPACE (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span>  <span class="hljs-number">1</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;white&quot;</span>))<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> MTTREE (<span class="hljs-name">rectangle</span> <span class="hljs-number">20</span> <span class="hljs-number">10</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;white&quot;</span>))<br></code></pre></td></tr></table></figure><p>接下来是函数设计，接受<code>BST</code>得到<code>Image</code>，获得一个简单的树图：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; BST -&gt; Image</span><br><span class="hljs-comment">;; produce a SIMPLE rendering of the tree</span><br></code></pre></td></tr></table></figure><p>写测试的时候就得把图像结构构思出来了：节点和子树用<code>above</code>，子树之间用<code>beside</code>，中间隔一下就行。</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-bst</span> false) MTTREE)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-bst</span> BST1) (<span class="hljs-name">above</span> (<span class="hljs-name">text</span> (<span class="hljs-name"><span class="hljs-built_in">string-append</span></span> <span class="hljs-string">&quot;1&quot;</span> KEY-VAL-SEPARATOR <span class="hljs-string">&quot;abc&quot;</span>)<br>                                             TEXT-SIZE<br>                                             TEXT-COLOR)<br>                                       VSPACE<br>                                       (<span class="hljs-name">beside</span> (<span class="hljs-name">render-bst</span> false)<br>                                               HSPACE<br>                                               (<span class="hljs-name">render-bst</span> false))))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-bst</span> BST4) (<span class="hljs-name">above</span> (<span class="hljs-name">text</span> (<span class="hljs-name"><span class="hljs-built_in">string-append</span></span> <span class="hljs-string">&quot;4&quot;</span> KEY-VAL-SEPARATOR <span class="hljs-string">&quot;dcj&quot;</span>)<br>                                             TEXT-SIZE<br>                                             TEXT-COLOR)<br>                                       VSPACE<br>                                       (<span class="hljs-name">beside</span> (<span class="hljs-name">render-bst</span> false)<br>                                               HSPACE<br>                                               (<span class="hljs-name">render-bst</span> (<span class="hljs-name">make-node</span> <span class="hljs-number">7</span> <span class="hljs-string">&quot;ruf&quot;</span> false false)))))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-bst</span> BST3) (<span class="hljs-name">above</span> (<span class="hljs-name">text</span> (<span class="hljs-name"><span class="hljs-built_in">string-append</span></span> <span class="hljs-string">&quot;3&quot;</span> KEY-VAL-SEPARATOR <span class="hljs-string">&quot;ilk&quot;</span>)<br>                                             TEXT-SIZE<br>                                             TEXT-COLOR)<br>                                       VSPACE<br>                                       (<span class="hljs-name">beside</span> (<span class="hljs-name">render-bst</span> BST1)<br>                                               HSPACE<br>                                               (<span class="hljs-name">render-bst</span> BST4))))<br></code></pre></td></tr></table></figure><p>桩函数：<code>;(define (render-bst t) (square 0 &quot;solid&quot; &quot;white&quot;))</code></p><p>实现起来就和测试差不多了，需要注意<code>node</code>的<code>key</code>是<code>number</code>类型的，拼接字符串时需要将其转换为字符串：</p><ul><li>如果树是<code>false</code>，那么为空树，则返回占位图<code>MTTREE</code></li><li>否则就跟测试一样生成图像</li></ul><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render-bst</span> t)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">false?</span> t) MTTREE]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name">above</span> (<span class="hljs-name">text</span> (<span class="hljs-name"><span class="hljs-built_in">string-append</span></span> (<span class="hljs-name"><span class="hljs-built_in">number-&gt;string</span></span> (<span class="hljs-name">node-key</span> t)) KEY-VAL-SEPARATOR (<span class="hljs-name">node-val</span> t))<br>                                             TEXT-SIZE<br>                                             TEXT-COLOR)<br>                                       VSPACE<br>                                       (<span class="hljs-name">beside</span> (<span class="hljs-name">render-bst</span> (<span class="hljs-name">node-l</span> t))<br>                                               HSPACE<br>                                               (<span class="hljs-name">render-bst</span> (<span class="hljs-name">node-r</span> t))))]))<br></code></pre></td></tr></table></figure><p>运行后4个测试通过，可以在交互区输入<code>(render-bst BST10)</code>，查看渲染效果。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:388/189;width:300px;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-binary-search-trees/render-bst-starter-test.webp" data-src="/images/2025/academics-ubc-cpsc-110-binary-search-trees/render-bst-starter-test.webp" alt="BST10 效果" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">BST10 效果</span></div></div><h2 id="practice-problems"><a class="markdownIt-Anchor" href="#practice-problems"></a> Practice Problems</h2><p>这一章的 Recommended Problems:</p><ul><li>BST P2 - Sum Keys<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/sum-keys-starter.rkt">sum-keys-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/sum-keys-solution.rkt">sum-keys-solution.rkt</a></li></ul></li><li>BST P4 - Insert<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/insert-starter.rkt">insert-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/insert-solution.rkt">insert-solution.rkt</a></li></ul></li><li>BST P6 - Render BST With Lines<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/render-bst-w-lines-starter.rkt">render-bst-w-lines-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/render-bst-w-lines-solution.rkt">render-bst-w-lines-solution.rkt</a></li></ul></li></ul><div class="tag-plugin colorful folders" ><details class="folder" index="0"><summary><p>BST P2 - Sum Keys 题解</p></summary><div class="body"><p><strong>预计耗时：7 min / 简单</strong></p><p>这道题让我们将一棵 BST 中所有的键加起来。这个函数的返回值是<code>Natural</code>，在判断时考虑空树：</p><ul><li>如果为空，返回<code>0</code></li><li>否则就将当前键加上左右子树递归的和（这样每次递归，都会将之后节点的键加起来）</li></ul><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; BST -&gt; Natural</span><br><span class="hljs-comment">;; sum all the keys in bst</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">sum-keys</span> false) <span class="hljs-number">0</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">sum-keys</span> BST3) (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">3</span> <span class="hljs-number">1</span> <span class="hljs-number">4</span> <span class="hljs-number">7</span>))<br><br><span class="hljs-comment">;&lt;template from BST&gt;</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">sum-keys</span> bst)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">false?</span> bst) <span class="hljs-number">0</span>]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name">node-key</span> bst)<br>            (<span class="hljs-name">sum-keys</span> (<span class="hljs-name">node-l</span> bst))<br>            (<span class="hljs-name">sum-keys</span> (<span class="hljs-name">node-r</span> bst)))]))<br></code></pre></td></tr></table></figure></div></details><details class="folder" index="1"><summary><p>BST P4 - Insert 题解</p></summary><div class="body"><p><strong>预计耗时：30 min / 中等</strong></p><p>这道题的难点在于我们应该想到在插入节点本身是在创建一个新的树，只不过刚好放个新节点进去，而不是在原有的树上进行修改。</p><p>在递归后，如果传入的树是空树，就意味着我们可以在这放新节点了，否则就在创建节点的同时顺便递归到左右子树：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">insert</span> key val bst)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">false?</span> bst) (<span class="hljs-name">make-node</span> key val false false)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> key (<span class="hljs-name">node-key</span> bst))<br>             (<span class="hljs-name">make-node</span> (<span class="hljs-name">node-key</span> bst) <br>                        (<span class="hljs-name">node-val</span> bst) <br>                        (<span class="hljs-name">insert</span> key val (<span class="hljs-name">node-l</span> bst))<br>                        (<span class="hljs-name">node-r</span> bst))<br>             (<span class="hljs-name">make-node</span> (<span class="hljs-name">node-key</span> bst) <br>                        (<span class="hljs-name">node-val</span> bst) <br>                        (<span class="hljs-name">node-l</span> bst)<br>                        (<span class="hljs-name">insert</span> key val (<span class="hljs-name">node-r</span> bst))))]))<br></code></pre></td></tr></table></figure></div></details><details class="folder" index="2"><summary><p>BST P6 - Render BST With Lines 题解</p></summary><div class="body"><p><strong>预计耗时：45 min / 困难</strong></p><p>绘图时总体上是垂直结构，包括<strong>当前节点</strong>、<strong>连接子树的线段</strong>和<strong>子树</strong>。</p><p>和之前某节一样，从上到下绘制即可，绘制到子树就直接递归，递归到空树就返回占位图<code>MTTREE</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render-bst</span> bst)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">false?</span> bst) MTTREE]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name">above</span> (<span class="hljs-name">text</span> (<span class="hljs-name"><span class="hljs-built_in">string-append</span></span> (<span class="hljs-name"><span class="hljs-built_in">number-&gt;string</span></span> (<span class="hljs-name">node-key</span> bst))<br>                       KEY-VAL-SEPARATOR<br>                       (<span class="hljs-name">node-val</span> bst))<br>                      TEXT-SIZE<br>                      TEXT-COLOR)<br>                (<span class="hljs-name">lines</span> (<span class="hljs-name">image-width</span> (<span class="hljs-name">render-bst</span> (<span class="hljs-name">node-l</span> bst)))<br>                       (<span class="hljs-name">image-width</span> (<span class="hljs-name">render-bst</span> (<span class="hljs-name">node-r</span> bst))))<br>                (<span class="hljs-name">beside</span> (<span class="hljs-name">render-bst</span> (<span class="hljs-name">node-l</span> bst))<br>                        (<span class="hljs-name">render-bst</span> (<span class="hljs-name">node-r</span> bst))))]))<br></code></pre></td></tr></table></figure></div></details></div>]]>
    </content>
    <id>https://ziling.moe/2025/academics-ubc-cpsc-110-binary-search-trees/</id>
    <link href="https://ziling.moe/2025/academics-ubc-cpsc-110-binary-search-trees/"/>
    <published>2025-08-12T03:30:00.000Z</published>
    <summary>UBC 的计科大一必修课 - CPSC 110</summary>
    <title>UBC - CPSC 110 - Binary Search Trees</title>
    <updated>2025-08-12T03:30:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term="Academics" scheme="https://ziling.moe/categories/Academics/"/>
    <category term="UBC" scheme="https://ziling.moe/tags/UBC/"/>
    <category term="计算机科学" scheme="https://ziling.moe/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    <category term="教程" scheme="https://ziling.moe/tags/%E6%95%99%E7%A8%8B/"/>
    <content>
      <![CDATA[<p>我们遇到的问题规模在扩大，但难度其实并没有增加很多。在遇到复杂问题的时候，我们需要学会拆开函数，设计一些<emp>辅助函数</emp> <em>(Helper functions)</em> 来帮助我们完成任务。</p><p>在之前，我们对于很复杂的设计问题，总是会自然地拆开一个函数用于简化代码编写。实际上，分而治之地解决一个大问题是一种非常重要的编程技巧。</p><h2 id="学习目标"><a class="markdownIt-Anchor" href="#学习目标"></a> 学习目标</h2><ul><li>引用其他非原始数据定义（这将包含在模板中）</li><li>能够组合函数</li><li>处理知识域 <em>(knowledge domain)</em> 迁移</li><li>操作任意规模的数据</li></ul><mark class="tag-plugin colorful mark" color="warning">以下内容涉及到的edX链接均不保证可访问性</mark><h2 id="function-composition"><a class="markdownIt-Anchor" href="#function-composition"></a> Function Composition</h2><p><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/arrange-images-starter.rkt">下载来自edX的 arrange-images-starter.rkt 文件</a>。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1118/499;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-helpers/arrange-image-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-helpers/arrange-image-starter.webp" alt="arrange-image-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">arrange-image-starter.rkt</span></div></div><p>这道题首先让我们设计一个数据定义，表示任意数量的图片。</p><blockquote><p>(A) Design a data definition to represent an arbitrary number of images.</p></blockquote><p>直接上手，从上一章对列表的数据定义开始：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfImage is one of:</span><br><span class="hljs-comment">;;   - empty</span><br><span class="hljs-comment">;;   - (cons Image ListOfImage)</span><br><span class="hljs-comment">;; interp. An arbitrary number of images</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LOI1 empty)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LOI2 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">20</span> <span class="hljs-number">30</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>) empty)))<br></code></pre></td></tr></table></figure><p>函数模板也和之前一样，处理空和非空两种情况，非空的情况需要处理<code>first</code>和<code>rest</code>递归：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme">#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-loi</span> loi)<br>    (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> loi) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>          [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>           (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">first</span> loi)<br>                (<span class="hljs-name">fn-for-loi</span> (<span class="hljs-name">rest</span> loi)))]))<br></code></pre></td></tr></table></figure><hr /><p>接下来就是第二个问题，设计一个叫做<code>arrange-images</code>的函数，接受一个<code>ListOfImage</code>，返回一个新的<code>Image</code>，把所有图片从左到右排列起来，且图片本身的大小逐渐增加。</p><blockquote><p>(B) Design a function called arrange-images that consumes an arbitrary number of images and lays them out left-to-right in increasing order of size.</p></blockquote><p>函数的签名、目的和例子：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfImage -&gt; Image</span><br><span class="hljs-comment">;; lay out images left to right in increasing order of size</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">arrange-images</span> empty) BLANK)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">arrange-images</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">20</span> <span class="hljs-number">30</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>) empty))) <br>                (<span class="hljs-name">beside</span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>) (<span class="hljs-name">rectangle</span> <span class="hljs-number">20</span> <span class="hljs-number">30</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>) blank))<br></code></pre></td></tr></table></figure><p>桩函数：<code>;(define (arrange-images loi) BLANK)  ; stub</code></p><p>也许你会觉得我们接下来会将函数模板复制过来直接写，但实际上我们需要提前思考一下这个问题该如何拆解，每涉及到对一种数据的操作，我们最好都需要设计一个函数来处理它：</p><ul><li>一个用来排序图片大小的函数</li><li>一个用来将图片从左到右排列的函数</li></ul><p>当然，<code>arrange-images</code>函数本身也是有内容的，就是调用上面两个函数：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">arrange-images</span> loi)<br>  (<span class="hljs-name">layout-images</span> (<span class="hljs-name">sort-images</span> loi)))<br></code></pre></td></tr></table></figure><pre class="mermaid">graph TD;    arrange_images(arrange-images) --> sort_images(sort-images);    arrange_images --> layout_images(layout-images);</pre><p>对于<code>layout-images</code>函数：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfImage -&gt; Image</span><br><span class="hljs-comment">;; place images beside each other in order of list</span><br><span class="hljs-comment">;; !!!</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">layout-images</span> loi) BLANK)<br></code></pre></td></tr></table></figure><p>对于<code>sort-images</code>函数：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfImage -&gt; ListOfImage</span><br><span class="hljs-comment">;; sort images in increasing order of size</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">sort-images</span> loi) loi)<br></code></pre></td></tr></table></figure><p>你会发现这个问题被拆成两个步骤了，虽然我们还没有实现这两个函数，但我们已经有了一个清晰的思路。</p><h2 id="laying-out-a-list-of-images"><a class="markdownIt-Anchor" href="#laying-out-a-list-of-images"></a> Laying Out a List of Images</h2><p>如果你的进度中断了，可以<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/arrange-images-v2.rkt">下载来自edX的 arrange-images-v2.rkt 文件</a>开始。</p><p>这一节我们会完善程序的<code>layout-images</code>函数，使其能够将图片从左到右排列。按照惯例，我们应当写一些测试：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">layout-images</span> empty) BLANK)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">layout-images</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>)<br>                                   (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)<br>                                         empty)))<br>              (<span class="hljs-name">beside</span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>)<br>                      (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)<br>                      BLANK))<br></code></pre></td></tr></table></figure><p>上一节剩下的<code>(define (layout-images loi) BLANK)</code>自然变成桩函数了。然后我们复制一个模板下来，开始写函数体：</p><ul><li><code>empty</code>的情况当然对应的就是<code>BLANK</code></li><li>非空的情况需要处理<code>first</code>和<code>rest</code>递归</li></ul><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">layout-images</span> loi)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> loi) BLANK]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name">beside</span> (<span class="hljs-name">first</span> loi)<br>                 (<span class="hljs-name">layout-images</span> (<span class="hljs-name">rest</span> loi)))]))<br></code></pre></td></tr></table></figure><p>当然，运行测试后会发现和预期结果不符，图像的顺序似乎反了。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:883/184;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-helpers/layout-images.webp" data-src="/images/2025/academics-ubc-cpsc-110-helpers/layout-images.webp" alt="与预期相反的图像" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">与预期相反的图像</span></div></div><p>但还好，至少<code>layout-images</code>的测试通过了。</p><h2 id="operating-on-a-list"><a class="markdownIt-Anchor" href="#operating-on-a-list"></a> Operating on a List</h2><p>如果你的进度中断了，可以<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/arrange-images-v3.rkt">下载来自edX的 arrange-images-v3.rkt 文件</a>开始，本节将会完成<code>sort-images</code>函数。</p><p>这个函数的目的很简单，就是将图像按大小从小到大排序，之后交由外层的<code>layout-images</code>函数进行布局。测试如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">sort-images</span> empty) empty)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">sort-images</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>)<br>                                 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">20</span> <span class="hljs-number">30</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)<br>                                       empty)))<br>              (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>)<br>                    (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">20</span> <span class="hljs-number">30</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)<br>                          empty)))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">sort-images</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">20</span> <span class="hljs-number">30</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)<br>                                  (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>)<br>                                        empty)))<br>              (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>)<br>                    (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">20</span> <span class="hljs-number">30</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)<br>                          empty)))<br><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">sort-images</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">30</span> <span class="hljs-number">40</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;green&quot;</span>)<br>                                  (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>)<br>                                        (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>)<br>                                              empty))))<br>              (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">30</span> <span class="hljs-number">40</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;green&quot;</span>)<br>                    (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>)<br>                          (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">20</span> <span class="hljs-number">30</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)<br>                                empty))))<br></code></pre></td></tr></table></figure><p>将之前的<code>(define (sort-images loi) loi)</code>注释上，复制函数模板，开始编写函数体。</p><ul><li><code>empty</code>自然就是<code>empty</code></li><li>非空的时候，我们需要将剩余的图像进行排序，然后将它们<strong>递归地放到第一位</strong>。我们该如何达到这一点呢？</li></ul><p>出于封装的角度考虑，我们可以实现一个<code>insert</code>函数，传入一个图像和一个已排序的图像列表，然后将图像插入到合适的位置返回：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Image ListOfImage -&gt; ListOfImage</span><br><span class="hljs-comment">;; insert img in proper place in 1st (in increasing order of size)</span><br><span class="hljs-comment">;; ASSUME: 1st is already sorted</span><br><span class="hljs-comment">;; !!!</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">insert</span> img <span class="hljs-number">1</span>st) <span class="hljs-number">1</span>st)<br></code></pre></td></tr></table></figure><pre class="mermaid">graph TD  A(arrange-images) --> B(sort-images)  A --> C(layout-images)  B --> D(insert)</pre><h2 id="domain-knowledge-shift"><a class="markdownIt-Anchor" href="#domain-knowledge-shift"></a> Domain Knowledge Shift</h2><p>如果你的进度中断了，可以<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/arrange-images-v4.rkt">下载来自edX的 arrange-images-v4.rkt 文件</a>开始。</p><p>回顾整个代码，我们会发现测试部分有很多地方是重复的，我们可以通过<code>define</code>常量来将它们抽出来，放在<code>Constants</code>的位置：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; for testing:</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> I1 (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> I2 (<span class="hljs-name">rectangle</span> <span class="hljs-number">20</span> <span class="hljs-number">30</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> I3 (<span class="hljs-name">rectangle</span> <span class="hljs-number">30</span> <span class="hljs-number">40</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;green&quot;</span>))<br></code></pre></td></tr></table></figure><p>之后，使用之前所学的<strong>全局替换</strong>将其他地方的测试用例中的图像替换为这些常量。</p><p>这样，我们的测试用例就能更轻易地维护，如需修改测试，大多数时候只需要修改常量的定义即可。</p><hr /><p>回到我们上一节没实现完的<code>insert</code>函数，我们可以开始更方便地写测试了：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">insert</span> I1 empty) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> I1 empty))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">insert</span> I1 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> I2 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> I3 empty))) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> I1 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> I2 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> I3 empty))))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">insert</span> I2 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> I1 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> I3 empty))) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> I1 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> I2 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> I3 empty))))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">insert</span> I3 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> I1 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> I2 empty))) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> I1 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> I2 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> I3 empty))))<br></code></pre></td></tr></table></figure><p>然后开始写函数体，可以从数据定义那复制过来。</p><ul><li>如果<code>loi</code>是<code>empty</code>，考虑到返回类型是<code>ListOfImage</code>，需要返回的其实是<code>(cons img empty)</code>而不是<code>empty</code>，不然会漏掉<code>img</code></li><li>如果<code>loi</code>是非空的，我们需要将<code>img</code>与<code>first</code>进行比较，然后决定将<code>img</code>放在<code>first</code>之前还是之后。</li></ul><p>比如说：</p><ul><li>对于<code>(insert I1 (cons I2 (cons I3 empty)))</code>测试：<ul><li><code>I1</code>比<code>I2</code>小，就将<code>I1</code>放在<code>I2</code>之前。</li></ul></li><li>对于<code>(insert I2 (cons I1 (cons I3 empty)))</code>测试：<ul><li><code>I2</code>比<code>I1</code>大，就将<code>I2</code>放在<code>I1</code>之后。</li></ul></li><li>对于<code>(insert I3 (cons I1 (cons I2 empty)))</code>测试：<ul><li><code>I3</code>比<code>I2</code>大，就将<code>I3</code>放在<code>I2</code>之后。</li></ul></li></ul><p>那么思路就比较清晰了，在<code>loi</code>非空的情况下，如果<code>img</code>比<code>first</code>大，那么就将<code>img</code>放在<code>first</code>之后，递归地将<code>img</code>插入到<code>rest</code>中；如果<code>img</code>比<code>first</code>小，那么就将<code>img</code>放在<code>first</code>之前，返回一个新的列表。</p><p>当然，我们也需要一个函数来帮我们判断图像大小，暂且称为<code>larger?</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">insert</span> img loi)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> loi) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> img empty)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name">larger?</span> img (<span class="hljs-name">first</span> loi))<br>             (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">first</span> loi)<br>                   (<span class="hljs-name">insert</span> img<br>                           (<span class="hljs-name">rest</span> loi)))<br>             (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> img loi))]))<br></code></pre></td></tr></table></figure><pre class="mermaid">graph TD  A(arrange-images) --> B(sort-images)  A --> C(layout-images)  B --> D(insert)  D --> E(larger?)</pre><h2 id="the-last-helper"><a class="markdownIt-Anchor" href="#the-last-helper"></a> The Last Helper</h2><p>如果你的进度中断了，可以<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/arrange-images-v5.rkt">下载来自edX的 arrange-images-v5.rkt 文件</a>开始。</p><p>书接上回，<code>larger?</code>函数需要接受两个图像，做一些与它们宽高相关的运算之后返回一个布尔值。测试如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">larger?</span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">3</span> <span class="hljs-number">4</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>) (<span class="hljs-name">rectangle</span> <span class="hljs-number">2</span> <span class="hljs-number">6</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">larger?</span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">5</span> <span class="hljs-number">4</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>) (<span class="hljs-name">rectangle</span> <span class="hljs-number">2</span> <span class="hljs-number">6</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)) true)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">larger?</span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">3</span> <span class="hljs-number">5</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>) (<span class="hljs-name">rectangle</span> <span class="hljs-number">2</span> <span class="hljs-number">6</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)) true)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">larger?</span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">3</span> <span class="hljs-number">4</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>) (<span class="hljs-name">rectangle</span> <span class="hljs-number">5</span> <span class="hljs-number">6</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">larger?</span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">3</span> <span class="hljs-number">4</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>) (<span class="hljs-name">rectangle</span> <span class="hljs-number">2</span> <span class="hljs-number">7</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)) false)<br></code></pre></td></tr></table></figure><p>函数体很简单，比较两个图像的面积即可：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">larger?</span> img1 img2)<br>  (<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> (<span class="hljs-name"><span class="hljs-built_in">*</span></span> (<span class="hljs-name">image-width</span> img1) (<span class="hljs-name">image-height</span> img1))<br>     (<span class="hljs-name"><span class="hljs-built_in">*</span></span> (<span class="hljs-name">image-width</span> img2) (<span class="hljs-name">image-height</span> img2))))<br></code></pre></td></tr></table></figure><p>运行后，所有的测试都通过了，我们完成了这道题。</p><h2 id="practice-problems"><a class="markdownIt-Anchor" href="#practice-problems"></a> Practice Problems</h2><p>这一章的 Recommended Problems:</p><ul><li>Helpers P2 - Making Rain Filtered<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/making-rain-filtered-starter.rkt">making-rain-filtered-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/making-rain-filtered-solution.rkt">making-rain-filtered-solution.rkt</a></li></ul></li></ul><div class="tag-plugin colorful folders" ><details class="folder" index="0"><summary><p>Helpers P2 - Making Rain Filtered 题解</p></summary><div class="body"><p><strong>预计耗时：120 min / 困难</strong></p><p>这道题让我们在一个天蓝色背景的窗口里显示多个下降的雨滴，同时鼠标点击的地方也能出现一个新的雨滴，需要注意的是，落出窗口外的雨滴不应当在之后的事件循环中被处理到。常数、数据定义等部分已经帮我们写好了，我们只需要处理<code>big-bang</code>即可。</p><p>此处的<code>big-bang</code>包含三个事件处理器：</p><ul><li><code>on-tick</code>：每隔一段时间更新雨滴的位置</li><li><code>on-mouse</code>：鼠标点击时生成新的雨滴</li><li><code>to-draw</code>：负责绘制所有雨滴</li></ul><hr /><p>首先是<code>on-tick</code>的<code>next-drops</code>函数，它的测试如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">next-drops</span> empty) empty)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">next-drops</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-drop</span> <span class="hljs-number">3</span> <span class="hljs-number">4</span>)<br>                                (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-drop</span> <span class="hljs-number">90</span> HEIGHT)<br>                                      empty)))<br>              (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-drop</span> <span class="hljs-number">3</span> <span class="hljs-number">5</span>)<br>                    empty))<br></code></pre></td></tr></table></figure><p>由于计算雨滴落下需要注意雨滴是否还在窗口内，我们需要将实际运算函数上套一个检测函数，比如：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">next-drops</span> lod)<br>  (<span class="hljs-name">onscreen-only</span> (<span class="hljs-name">tick-drops</span> lod)))<br></code></pre></td></tr></table></figure><p>对于<code>tick-drops</code>函数，我们需要更新每个水滴的位置 —— 给它的<code>y</code>坐标加上<code>SPEED</code>即可。由于这个函数同样需要返回<code>ListOfDrop</code>，所以在递归调用的同时需要用<code>cons</code>存住：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">tick-drops</span> lod)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> lod) empty]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> <br>         (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-drop</span> (<span class="hljs-name">drop-x</span> (<span class="hljs-name">first</span> lod)) (<span class="hljs-name"><span class="hljs-built_in">+</span></span> SPEED (<span class="hljs-name">drop-y</span> (<span class="hljs-name">first</span> lod))))<br>               (<span class="hljs-name">tick-drops</span> (<span class="hljs-name">rest</span> lod)))]))<br></code></pre></td></tr></table></figure><p>这样<strong>下落</strong>的效果就做好了，接下来我们需要处理<strong>屏幕外</strong>的水滴。由于其本质是<strong>筛选</strong>的过程，所以在递归过程中，每个<code>first</code>都应该去检查其<code>y</code>坐标是否还在窗口内，如果在就算上，否则就带着剩下的<code>lod</code>递归：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">onscreen-only</span> lod)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> lod) empty]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">&lt;=</span></span> <span class="hljs-number">0</span> (<span class="hljs-name">drop-y</span> (<span class="hljs-name">first</span> lod)) HEIGHT)<br>             (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">first</span> lod) (<span class="hljs-name">onscreen-only</span> (<span class="hljs-name">rest</span> lod)))<br>             (<span class="hljs-name">onscreen-only</span> (<span class="hljs-name">rest</span> lod)))]))<br></code></pre></td></tr></table></figure><hr /><p>接下来实现<code>render-drops</code>，渲染最复杂的就是我们如何在渲染<code>first</code>的同时又能让剩下的接着递归渲染。我们可以写一个<code>place-drop</code>函数来帮我们做到这一点：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render-drops</span> lod)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> lod) MTS]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> <br>         (<span class="hljs-name">place-drop</span> (<span class="hljs-name">first</span> lod)<br>                     (<span class="hljs-name">render-drops</span> (<span class="hljs-name">rest</span> lod)))]))<br></code></pre></td></tr></table></figure><p><code>place-drop</code>接受当前水滴的数据和剩下待处理的水滴。第二个参数让渲染变得可以递归，每次渲染后都能调回<code>render-drops</code>进行处理：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">place-drop</span> d img)<br>  (<span class="hljs-name">place-image</span> DROP (<span class="hljs-name">drop-x</span> d) (<span class="hljs-name">drop-y</span> d) img))<br></code></pre></td></tr></table></figure><hr /><p>最后就是鼠标点击事件的响应函数<code>handle-mouse</code>了，它本身能传入当前<code>lod</code>，鼠标点击的坐标和行为类型。我们只需要它的点击事件就行，其他的就原封不动返回原列表：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">handle-mouse</span> lod x y mevt)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">mouse=?</span> mevt <span class="hljs-string">&quot;button-down&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-drop</span> x y) lod)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> lod]))<br></code></pre></td></tr></table></figure></div></details></div>]]>
    </content>
    <id>https://ziling.moe/2025/academics-ubc-cpsc-110-helpers/</id>
    <link href="https://ziling.moe/2025/academics-ubc-cpsc-110-helpers/"/>
    <published>2025-08-08T00:00:00.000Z</published>
    <summary>UBC 的计科大一必修课 - CPSC 110</summary>
    <title>UBC - CPSC 110 - Helpers</title>
    <updated>2025-08-08T00:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term="Academics" scheme="https://ziling.moe/categories/Academics/"/>
    <category term="UBC" scheme="https://ziling.moe/tags/UBC/"/>
    <category term="计算机科学" scheme="https://ziling.moe/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    <category term="教程" scheme="https://ziling.moe/tags/%E6%95%99%E7%A8%8B/"/>
    <content>
      <![CDATA[<p>本模块将专注于自然数与函数，也会涉及到<strong>自我引用</strong>的部分。</p><h2 id="学习目标"><a class="markdownIt-Anchor" href="#学习目标"></a> 学习目标</h2><ul><li>能够设计对自然数进行操作的函数</li><li>能够设计自然数的简单替代表示</li></ul><mark class="tag-plugin colorful mark" color="warning">以下内容涉及到的edX链接均不保证可访问性</mark><h2 id="natural-numbers"><a class="markdownIt-Anchor" href="#natural-numbers"></a> Natural Numbers</h2><p>自然数有无数个，所以我们可以借助自我引用来实现一个从给定自然数倒计时到0的函数。</p><p>在此之前，先介绍一个新表达式：<code>add1</code>和<code>sub1</code>。它们分别表示对自然数加1和减1，其使用方法和行为很像<code>cons</code>和<code>rest</code>。</p><p>比如<code>(add1 0)</code>的结果是<code>1</code>，而<code>(sub1 1)</code>的结果是<code>0</code>。你也可以嵌套着使用它们，比如<code>(add1 (sub1 2))</code>的结果是<code>2</code>。</p><p><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/naturals-starter.rkt">下载来自edX的 naturals-starter.rkt 文件</a>，里面给了一个倒数自然数的数据定义：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Natural is one of:</span><br><span class="hljs-comment">;;  - 0</span><br><span class="hljs-comment">;;  - (add1 Natural)</span><br><span class="hljs-comment">;; interp. a natural number</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> N0 <span class="hljs-number">0</span>)         <span class="hljs-comment">;0</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> N1 (<span class="hljs-name">add1</span> N0)) <span class="hljs-comment">;1</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> N2 (<span class="hljs-name">add1</span> N1)) <span class="hljs-comment">;2</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-natural</span> n)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">zero?</span></span> n) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">...</span></span> <span class="hljs-comment">;n                           ;template rules wouldn&#x27;t normally put this</span><br>          <span class="hljs-comment">;                            ;here, but we will see that we end up coming</span><br>          <span class="hljs-comment">;                            ;back to add it</span><br>          (<span class="hljs-name">fn-for-natural</span> (<span class="hljs-name">sub1</span> n)))]))<br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;  - one-of: two cases</span><br><span class="hljs-comment">;;  - atomic distinct: 0</span><br><span class="hljs-comment">;;  - compound: (add1 Natural)</span><br><span class="hljs-comment">;;  - self-reference: (sub1 n) is Natural</span><br></code></pre></td></tr></table></figure><p>下面还有两道题，让我们来看第一道，计算从0到n的自然数之和。</p><blockquote><p>Design a function that consumes Natural number n and produces the sum of all the naturals in [0, n].</p></blockquote><p>我们称该函数为<code>sum</code>，签名是<code>Natural -&gt; Natural</code>，目的是<code>produce sum of Natural[0, n]</code>，然后测试有：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">sum</span> <span class="hljs-number">0</span>) <span class="hljs-number">0</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">sum</span> <span class="hljs-number">1</span>) <span class="hljs-number">1</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">sum</span> <span class="hljs-number">3</span>) (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">3</span> <span class="hljs-number">2</span> <span class="hljs-number">1</span> <span class="hljs-number">0</span>))  <span class="hljs-comment">; 倒序</span><br></code></pre></td></tr></table></figure><p>桩函数：<code>;(define (sum n) 0)</code>。然后就可以从函数模板复制过来开始实现函数体了。</p><p>从函数的递归过程能发现，函数应该做到<code>n + (n-1) + ... + 1 + 0</code>，我们知道每次加的操作都是由一次递归调用来实现的，所以可以这样写<code>n + (sum (sub1 n))</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">sum</span> n)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">zero?</span></span> n) <span class="hljs-number">0</span>]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">+</span></span> n (<span class="hljs-name">sum</span> (<span class="hljs-name">sub1</span> n)))]))<br></code></pre></td></tr></table></figure><p>接下来是第二道题，将传入的自然数转换为一个列表，列表的元素是从该自然数开始递减到0的所有自然数。</p><blockquote><p>Design a function that consumes Natural number n and produces a list of all the naturals of the form (cons n (cons n-1 … empty)) not including 0.</p></blockquote><p>我们称该函数为<code>to-list</code>，签名是<code>Natural -&gt; ListOfNatural</code>，目的是<code>produce (cons n (cons n-1 ... empty)), not including 0</code>，然后测试有：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">to-list</span> <span class="hljs-number">0</span>) empty)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">to-list</span> <span class="hljs-number">1</span>) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">1</span> empty))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">to-list</span> <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">2</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">1</span> empty)))<br></code></pre></td></tr></table></figure><p>写完桩函数<code>;(define (to-list n) empty)</code>，同理复制函数模板开始实现函数体。</p><p>确认终止条件，即<code>zero? n</code>满足时应当返回<code>empty</code>，否则就需要递归调用<code>to-list</code>，并将当前的<code>n</code>添加到列表：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">to-list</span> n)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">zero?</span></span> n) empty]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> n (<span class="hljs-name">to-list</span> (<span class="hljs-name">sub1</span> n)))]))<br></code></pre></td></tr></table></figure><hr /><p>从这两道题能看出不仅仅是列表能使用自我引用来遍历，自然数也可以做到这种效果。</p><p><em>ps: 该不会这语言的循环都是通过递归实现的吧？</em></p><h2 id="a-parlor-trick"><a class="markdownIt-Anchor" href="#a-parlor-trick"></a> A Parlor Trick</h2><p>如果 Racket 语言没有自然数类型该怎么办？我们可以通过 HtDD 和 HtDF 来定义。<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/new-numerals-starter.rkt">下载来自edX的 new-numerals-starter.rkt 文件</a>，描述说这个 Racket 语言甚至没有数字，但你需要自然数来编程。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1275/529;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-naturals/new-numerals-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-naturals/new-numerals-starter.webp" alt="new-numerals-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">new-numerals-starter.rkt</span></div></div><p>好在这个 Racket 还是有列表、字符串什么的，我们可以自己定义一个自然数试试，为了避免混淆，我们把它叫做<code>NATURAL</code>。这个<code>NATURAL</code>将会是一个列表，列表的元素都是<code>&quot;1&quot;</code>或者<code>empty</code>，通过列表长度来表示自然数的大小。</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; NATURAL is one of:</span><br><span class="hljs-comment">;;   - empty</span><br><span class="hljs-comment">;;   - (cons &quot;1&quot; NATURAL)</span><br><span class="hljs-comment">;; interp. a natural number, the number of &quot;1&quot; in the list is the number</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> N0 empty)         <span class="hljs-comment">;0</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> N1 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;1&quot;</span> N0)) <span class="hljs-comment">;1</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> N2 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;1&quot;</span> N1)) <span class="hljs-comment">;2</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> N3 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;1&quot;</span> N2)) <span class="hljs-comment">;3</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> N4 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;1&quot;</span> N3)) <span class="hljs-comment">;4</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> N5 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;1&quot;</span> N4)) <span class="hljs-comment">;5</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> N6 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;1&quot;</span> N5)) <span class="hljs-comment">;6</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> N7 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;1&quot;</span> N6)) <span class="hljs-comment">;7</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> N8 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;1&quot;</span> N7)) <span class="hljs-comment">;8</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> N9 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;1&quot;</span> N8)) <span class="hljs-comment">;9</span><br></code></pre></td></tr></table></figure><p>在之前使用<code>Natural</code>时用到的<code>zero?</code>也可以实现了，只需要判断<code>NATURAL</code>是否是<code>empty</code>即可：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; These are the primitives that operate NATURAL:</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">ZERO?</span> n) (<span class="hljs-name">empty?</span> n))       <span class="hljs-comment">; Any -&gt; Boolean</span><br></code></pre></td></tr></table></figure><p>同理，<code>add1</code>和<code>sub1</code>也可以实现。比如<code>ADD1</code>给列表加一个<code>&quot;1&quot;</code>，<code>SUB1</code>则是借助<code>rest</code>表达式去掉列表的一个<code>&quot;1&quot;</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">ADD1</span> n) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;1&quot;</span> n))      <span class="hljs-comment">; NATURAL     -&gt; NATURAL</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">SUB1</span> n) (<span class="hljs-name">rest</span> n))          <span class="hljs-comment">; NATURAL[&gt;0] -&gt; NATURAL</span><br></code></pre></td></tr></table></figure><p><em>ps: 之后的章节没要求就不需要写 Template rules used 了</em></p><p>函数模板可以写成逐渐减一的形式：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-NATURAL</span> n)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">ZERO?</span> n) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">...</span></span> <span class="hljs-comment">;n</span><br>          (<span class="hljs-name">fn-for-NATURAL</span> (<span class="hljs-name">SUB1</span> n)))]))<br></code></pre></td></tr></table></figure><hr /><p>自定义的<code>NATURAL</code>类型也需要基本运算的支持，比如加法<code>ADD</code>，它的签名是<code>NATURAL NATURAL -&gt; NATURAL</code>，目的是<code>produce a + b</code>，测试有：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">ADD</span> N2 N0) N2)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">ADD</span> N0 N3) N3)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">ADD</span> N3 N4) N7)<br></code></pre></td></tr></table></figure><p>桩函数：<code>;(define (ADD a b) N0)</code>。然后复制函数模板开始实现函数体：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">ADD</span> a b)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">ZERO?</span> a) b]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name">ADD</span> (<span class="hljs-name">SUB1</span> a) (<span class="hljs-name">ADD1</span> b))]))<br></code></pre></td></tr></table></figure><p>看起来行为很奇怪，实际上这个函数的递归过程是将<code>a</code>逐渐减1，同时将<code>b</code>逐渐加1，直到<code>a</code>变成0时返回<code>b</code>。</p><p>减法同理：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; NATURAL NATURAL -&gt; NATURAL</span><br><span class="hljs-comment">;; produce a - b</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">SUBTRACT</span> N2 N0) N2)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">SUBTRACT</span> N6 N2) N4)<br><br><span class="hljs-comment">;(define (SUBTRACT a b) N0)  ; stub</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">SUBTRACT</span> a b)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">ZERO?</span> b) a]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name">SUBTRACT</span> (<span class="hljs-name">SUB1</span> a) (<span class="hljs-name">SUB1</span> b))]))<br></code></pre></td></tr></table></figure><p>至此，我们已经实现了一个简单的自然数类型，并且支持了加减法运算。</p><h2 id="practice-problems"><a class="markdownIt-Anchor" href="#practice-problems"></a> Practice Problems</h2><p>这一章的 Recommended Problems:</p><ul><li>Naturals P2 - Decreasing Image<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/decreasing-image-starter.rkt">decreasing-image-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/decreasing-image-solution.rkt">decreasing-image-solution.rkt</a></li></ul></li><li>Naturals P3 - Odd from n<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/odd-from-n-starter.rkt">odd-from-n-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/odd-from-n-solution.rkt">odd-from-n-solution.rkt</a></li></ul></li></ul><div class="tag-plugin colorful folders" ><details class="folder" index="0"><summary><p>Naturals P2 - Decreasing Image 题解</p></summary><div class="body"><p><strong>预计耗时：15 min / 中等</strong></p><p>这道题的递归思路比较绕，我们需要考虑到显示多个<code>text</code>是需要用<code>beside</code>来布局的，因此在递归的过程中需要将每个<code>text</code>都放在一个<code>beside</code>中，同时将下一个递归调用也放在里面。</p><p><em>ps: 以后可能只贴核心代码了</em></p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">decreasing-image</span> n)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">zero?</span></span> n) (<span class="hljs-name">text</span> <span class="hljs-string">&quot;0&quot;</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;black&quot;</span>)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name">beside</span> (<span class="hljs-name">text</span> (<span class="hljs-name"><span class="hljs-built_in">number-&gt;string</span></span> n) <span class="hljs-number">20</span> <span class="hljs-string">&quot;black&quot;</span>)<br>                 (<span class="hljs-name">text</span> <span class="hljs-string">&quot; &quot;</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;black&quot;</span>)  <span class="hljs-comment">; spacing</span><br>                 (<span class="hljs-name">decreasing-image</span> (<span class="hljs-name">sub1</span> n)))]))<br></code></pre></td></tr></table></figure></div></details><details class="folder" index="1"><summary><p>Naturals P3 - Odd from n 题解</p></summary><div class="body"><p><strong>预计耗时：15 min / 中等</strong></p><p>这道题较简单，和之前用<code>cons</code>一样，我们只需要判断<code>n</code>是否为0，如果是0则返回<code>empty</code>，否则判断<code>n</code>是否为奇数，如果是奇数则将其添加到列表中，否则直接递归调用即可。</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">odd-from-n</span> n)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">zero?</span></span> n) empty]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">odd?</span></span> n)<br>             (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> n (<span class="hljs-name">odd-from-n</span> (<span class="hljs-name">sub1</span> n)))<br>             (<span class="hljs-name">odd-from-n</span> (<span class="hljs-name">sub1</span> n)))]))<br></code></pre></td></tr></table></figure></div></details></div>]]>
    </content>
    <id>https://ziling.moe/2025/academics-ubc-cpsc-110-naturals/</id>
    <link href="https://ziling.moe/2025/academics-ubc-cpsc-110-naturals/"/>
    <published>2025-08-05T08:20:00.000Z</published>
    <summary>UBC 的计科大一必修课 - CPSC 110</summary>
    <title>UBC - CPSC 110 - Naturals</title>
    <updated>2025-08-05T08:20:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term="Academics" scheme="https://ziling.moe/categories/Academics/"/>
    <category term="UBC" scheme="https://ziling.moe/tags/UBC/"/>
    <category term="计算机科学" scheme="https://ziling.moe/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    <category term="教程" scheme="https://ziling.moe/tags/%E6%95%99%E7%A8%8B/"/>
    <content>
      <![CDATA[<p>本章会涉及到更加复杂的数据结构设计，在很多时候我们不会仅靠一个数据定义就解决问题。</p><h2 id="学习目标"><a class="markdownIt-Anchor" href="#学习目标"></a> 学习目标</h2><ul><li>能够预测和识别数据定义中的引用与操作该数据的函数中的辅助函数调用之间的对应关系</li></ul><mark class="tag-plugin colorful mark" color="warning">以下内容涉及到的edX链接均不保证可访问性</mark><h2 id="the-reference-rule"><a class="markdownIt-Anchor" href="#the-reference-rule"></a> The Reference Rule</h2><h3 id="p1"><a class="markdownIt-Anchor" href="#p1"></a> P1</h3><p><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/tuition-graph-starter.rkt">下载来自edX的 tuition-graph-starter.rkt 文件</a>。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1882/1472;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-reference/tuition-graph-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-reference/tuition-graph-starter.webp" alt="tuition-graph-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">tuition-graph-starter.rkt</span></div></div><p>这道题简单来说就是帮助一个人选择上哪所大学，其中有个很重要的考虑因素就是学费，也想将学费的对比通过柱状图可视化出来。</p><p>第一个面临的问题就是定义一个能够涵盖不同大学学费的数据结构。之后还需要设计一个用来生成柱状图的函数。最后写一个找到最低学费并给出判断的函数。</p><p>首先这不需要设计一个世界，只是图像绘制加一些分析而已。</p><p>在定义数据类型之前，我们先在代码文件顶部写上<code>(require 2htdp/image)</code>以加入图像支持。</p><p>构思一遍程序设计的全过程，我们可以提前为柱状图这个图像写一些常量：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> FONT-SIZE <span class="hljs-number">24</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> FONT-COLOR <span class="hljs-string">&quot;black&quot;</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> Y-SCALE <span class="hljs-number">1/200</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> BAR-WIDTH <span class="hljs-number">30</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> BAR-COLOR <span class="hljs-string">&quot;lightblue&quot;</span>)<br></code></pre></td></tr></table></figure><p>由于学费通常在<code>$20,000</code>左右，而我们的显示器通常难以容纳数以万计的像素，故其中<code>Y-SCALE</code>是很必要的，设置成比如<code>1/200</code>的值可以让学费的值变得可被直观比较。</p><hr /><p>接下来就是数据结构设计，我们可以记录每个学校的名字和学费：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">define-struct</span> school (<span class="hljs-name">name</span> tuition))<br><span class="hljs-comment">;; School is (make-school String Natural)</span><br><span class="hljs-comment">;; interp. name is the school&#x27;s name, tuition is international student&#x27;s tuition in USD</span><br></code></pre></td></tr></table></figure><p>以及它的例子，填上你喜欢的学校也是可以的：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> S1 (<span class="hljs-name">make-school</span> <span class="hljs-string">&quot;School1&quot;</span> <span class="hljs-number">27797</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> S2 (<span class="hljs-name">make-school</span> <span class="hljs-string">&quot;School2&quot;</span> <span class="hljs-number">23300</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> S3 (<span class="hljs-name">make-school</span> <span class="hljs-string">&quot;School3&quot;</span> <span class="hljs-number">28500</span>))<br></code></pre></td></tr></table></figure><p>对于函数模板，需要记住要把<code>School</code>的两个字段都算上：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-school</span> s)<br>    (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">school-name</span> s)<br>         (<span class="hljs-name">school-tuition</span>)))<br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;   - compound: (make-school String Natural)</span><br></code></pre></td></tr></table></figure><hr /><p><code>School</code>只是一个元素，我们需要的数据结构还需要能把一串学校给记录下来。我们就称其为<code>ListOfSchool</code>，里面的每个元素类型则是我们自定义的<code>School</code>，回忆之前定义列表的过程：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfSchool is one of:</span><br><span class="hljs-comment">;;   - empty</span><br><span class="hljs-comment">;;   - (cons School ListOfSchool)</span><br><span class="hljs-comment">;; interp. a list of schools</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LOS1 empty)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LOS2 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> S1 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> S2 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> S3 empty))))<br></code></pre></td></tr></table></figure><p>在之前，我们只会在列表中遇到一个<strong>自我引用</strong>，即其自身包含与其相同类型的值。而这里我们将它的元素类型也变成自定义的了。</p><h3 id="p2"><a class="markdownIt-Anchor" href="#p2"></a> P2</h3><p>我们可以从之前的进度，或者<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/tuition-graph-v3.rkt">edX 的 tuition-graph-v3.rkt</a>开始实现函数。</p><p>第一个就是通过学校列表得到柱状图的函数：<code>ListOfSchool -&gt; Image</code>，其目的是<code>produce bar chart showing names and tuitions of consumed schools</code>，称该函数为<code>chart</code>。</p><p>桩函数是：<code>(define (chart los) (square 0 &quot;solid&quot; &quot;white&quot;))</code></p><p>先写它的测试，先是简单的<code>empty</code>，再是有一个学校的情况。对于一根柱子，我们需要用到<code>rectangle</code>，文本则是<code>text</code>。文本的旋转靠<code>rotate</code>。在算柱子高度的时候需要考虑<code>Y-SCALE</code>。</p><p>由于我们需要控制每个图像元素的相对方位，比如柱子整体是靠下而不是靠上的。文本也是居中向下对齐的，我们就可以使用<code>beside/align</code>或是<code>overlay/align</code>等，传入后面元素来排版。</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">chart</span> empty) (<span class="hljs-name">square</span> <span class="hljs-number">0</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;white&quot;</span>))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">chart</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-school</span> <span class="hljs-string">&quot;S1&quot;</span> <span class="hljs-number">8000</span>) empty))<br>              (<span class="hljs-name">beside/align</span> <span class="hljs-string">&quot;bottom&quot;</span><br>                            (<span class="hljs-name">overlay/align</span> <span class="hljs-string">&quot;center&quot;</span> <span class="hljs-string">&quot;bottom&quot;</span><br>                                           (<span class="hljs-name">rotate</span> <span class="hljs-number">90</span> (<span class="hljs-name">text</span>  <span class="hljs-string">&quot;S1&quot;</span> FONT-SIZE FONT-COLOR))<br>                                           (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">8000</span> Y-SCALE) <span class="hljs-string">&quot;outline&quot;</span> <span class="hljs-string">&quot;black&quot;</span>)<br>                                           (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">8000</span> Y-SCALE) <span class="hljs-string">&quot;solid&quot;</span> BAR-COLOR))<br>                            (<span class="hljs-name">square</span> <span class="hljs-number">0</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;white&quot;</span>)))<br><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">chart</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-school</span> <span class="hljs-string">&quot;S2&quot;</span> <span class="hljs-number">12000</span>) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-school</span> <span class="hljs-string">&quot;S1&quot;</span> <span class="hljs-number">8000</span>) empty)))<br>              (<span class="hljs-name">beside/align</span> <span class="hljs-string">&quot;bottom&quot;</span><br>                            (<span class="hljs-name">overlay/align</span> <span class="hljs-string">&quot;center&quot;</span> <span class="hljs-string">&quot;bottom&quot;</span><br>                                           (<span class="hljs-name">rotate</span> <span class="hljs-number">90</span> (<span class="hljs-name">text</span> <span class="hljs-string">&quot;S2&quot;</span> FONT-SIZE FONT-COLOR))<br>                                           (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">12000</span> Y-SCALE) <span class="hljs-string">&quot;outline&quot;</span> <span class="hljs-string">&quot;black&quot;</span>)<br>                                           (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">12000</span> Y-SCALE) <span class="hljs-string">&quot;solid&quot;</span> BAR-COLOR))<br>                            (<span class="hljs-name">overlay/align</span> <span class="hljs-string">&quot;center&quot;</span> <span class="hljs-string">&quot;bottom&quot;</span><br>                                          (<span class="hljs-name">rotate</span> <span class="hljs-number">90</span> (<span class="hljs-name">text</span> <span class="hljs-string">&quot;S1&quot;</span> FONT-SIZE FONT-COLOR))<br>                                          (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">8000</span> Y-SCALE) <span class="hljs-string">&quot;outline&quot;</span> <span class="hljs-string">&quot;black&quot;</span>)<br>                                          (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">8000</span> Y-SCALE) <span class="hljs-string">&quot;solid&quot;</span> BAR-COLOR))<br>                            (<span class="hljs-name">square</span> <span class="hljs-number">0</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;white&quot;</span>)))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">chart</span> los) (<span class="hljs-name">square</span> <span class="hljs-number">0</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;white&quot;</span>))<br></code></pre></td></tr></table></figure><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1434/370;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-reference/tuition-graph-test-window.webp" data-src="/images/2025/academics-ubc-cpsc-110-reference/tuition-graph-test-window.webp" alt="运行后测试窗口" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">运行后测试窗口</span></div></div><h3 id="p3"><a class="markdownIt-Anchor" href="#p3"></a> P3</h3><p>最后就是<code>chart</code>的实现了，结合之前的经验，我们可以先按照模板改成这样：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">chart</span> los)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> los) (<span class="hljs-name">square</span> <span class="hljs-number">0</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;white&quot;</span>)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name">beside/align</span> <span class="hljs-string">&quot;bottom&quot;</span> <br>              (<span class="hljs-name">make-bar</span> (<span class="hljs-name">first</span> los))<br>              (<span class="hljs-name">chart</span> (<span class="hljs-name">rest</span> los)))]))<br></code></pre></td></tr></table></figure><p>其中，<code>beside/align</code>可以将我们发现的每个柱子挨个排着，同时出于封装角度考虑，里面我们也将要实现<code>make-bar</code>函数来将单个柱子绘制出来贴上去。</p><p><code>make-bar</code>的签名自然就是<code>School -&gt; Image</code>，目的是<code>produce the bar for a single school in the bar chart</code>。测试就是尝试绘制<code>S1</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">make-bar</span> (<span class="hljs-name">make-school</span> <span class="hljs-string">&quot;S1&quot;</span> <span class="hljs-number">8000</span>))<br>              (<span class="hljs-name">overlay/align</span> <span class="hljs-string">&quot;center&quot;</span> <span class="hljs-string">&quot;bottom&quot;</span><br>                             (<span class="hljs-name">rotate</span> <span class="hljs-number">90</span> (<span class="hljs-name">text</span> <span class="hljs-string">&quot;S1&quot;</span> FONT-SIZE FONT-COLOR))<br>                             (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">8000</span> Y-SCALE) <span class="hljs-string">&quot;outline&quot;</span> <span class="hljs-string">&quot;black&quot;</span>)<br>                             (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">8000</span> Y-SCALE) <span class="hljs-string">&quot;solid&quot;</span> BAR-COLOR)))<br></code></pre></td></tr></table></figure><p>桩函数是<code>;(define (make-bar s) (square 0 &quot;solid&quot; &quot;white&quot;))  ; stub</code></p><p>可以根据测试，把函数体实现了：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">make-bar</span> s)<br>        (<span class="hljs-name">overlay/align</span> <span class="hljs-string">&quot;center&quot;</span> <span class="hljs-string">&quot;bottom&quot;</span><br>                       (<span class="hljs-name">rotate</span> <span class="hljs-number">90</span> (<span class="hljs-name">text</span> (<span class="hljs-name">school-name</span> s) FONT-SIZE FONT-COLOR))<br>                       (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> (<span class="hljs-name">school-tuition</span> s) Y-SCALE) <span class="hljs-string">&quot;outline&quot;</span> <span class="hljs-string">&quot;black&quot;</span>)<br>                       (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> (<span class="hljs-name">school-tuition</span> s) Y-SCALE) <span class="hljs-string">&quot;solid&quot;</span> BAR-COLOR)))<br></code></pre></td></tr></table></figure><p>最后运行，在交互区先输入<code>(chart LOS1)</code>回车，再输出<code>(chart LOS2)</code>回车，就能得到一个有三个<code>School</code>的柱状图。</p><p>函数部分代码如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">chart</span> empty) (<span class="hljs-name">square</span> <span class="hljs-number">0</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;white&quot;</span>))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">chart</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-school</span> <span class="hljs-string">&quot;S1&quot;</span> <span class="hljs-number">8000</span>) empty))<br>              (<span class="hljs-name">beside/align</span> <span class="hljs-string">&quot;bottom&quot;</span><br>                            (<span class="hljs-name">overlay/align</span> <span class="hljs-string">&quot;center&quot;</span> <span class="hljs-string">&quot;bottom&quot;</span><br>                                           (<span class="hljs-name">rotate</span> <span class="hljs-number">90</span> (<span class="hljs-name">text</span>  <span class="hljs-string">&quot;S1&quot;</span> FONT-SIZE FONT-COLOR))<br>                                           (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">8000</span> Y-SCALE) <span class="hljs-string">&quot;outline&quot;</span> <span class="hljs-string">&quot;black&quot;</span>)<br>                                           (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">8000</span> Y-SCALE) <span class="hljs-string">&quot;solid&quot;</span> BAR-COLOR))<br>                            (<span class="hljs-name">square</span> <span class="hljs-number">0</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;white&quot;</span>)))<br><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">chart</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-school</span> <span class="hljs-string">&quot;S2&quot;</span> <span class="hljs-number">12000</span>) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-school</span> <span class="hljs-string">&quot;S1&quot;</span> <span class="hljs-number">8000</span>) empty)))<br>              (<span class="hljs-name">beside/align</span> <span class="hljs-string">&quot;bottom&quot;</span><br>                            (<span class="hljs-name">overlay/align</span> <span class="hljs-string">&quot;center&quot;</span> <span class="hljs-string">&quot;bottom&quot;</span><br>                                           (<span class="hljs-name">rotate</span> <span class="hljs-number">90</span> (<span class="hljs-name">text</span> <span class="hljs-string">&quot;S2&quot;</span> FONT-SIZE FONT-COLOR))<br>                                           (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">12000</span> Y-SCALE) <span class="hljs-string">&quot;outline&quot;</span> <span class="hljs-string">&quot;black&quot;</span>)<br>                                           (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">12000</span> Y-SCALE) <span class="hljs-string">&quot;solid&quot;</span> BAR-COLOR))<br>                            (<span class="hljs-name">overlay/align</span> <span class="hljs-string">&quot;center&quot;</span> <span class="hljs-string">&quot;bottom&quot;</span><br>                                          (<span class="hljs-name">rotate</span> <span class="hljs-number">90</span> (<span class="hljs-name">text</span> <span class="hljs-string">&quot;S1&quot;</span> FONT-SIZE FONT-COLOR))<br>                                          (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">8000</span> Y-SCALE) <span class="hljs-string">&quot;outline&quot;</span> <span class="hljs-string">&quot;black&quot;</span>)<br>                                          (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">8000</span> Y-SCALE) <span class="hljs-string">&quot;solid&quot;</span> BAR-COLOR))<br>                            (<span class="hljs-name">square</span> <span class="hljs-number">0</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;white&quot;</span>)))<br><br><span class="hljs-comment">;(define (chart los) (square 0 &quot;solid&quot; &quot;white&quot;))  ; stub</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">chart</span> los)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> los) (<span class="hljs-name">square</span> <span class="hljs-number">0</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;white&quot;</span>)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name">beside/align</span> <span class="hljs-string">&quot;bottom&quot;</span> <br>              (<span class="hljs-name">make-bar</span> (<span class="hljs-name">first</span> los))<br>              (<span class="hljs-name">chart</span> (<span class="hljs-name">rest</span> los)))]))<br><br><span class="hljs-comment">;; School -&gt; Image</span><br><span class="hljs-comment">;; produce the bar for a single school in the bar chart</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">make-bar</span> (<span class="hljs-name">make-school</span> <span class="hljs-string">&quot;S1&quot;</span> <span class="hljs-number">8000</span>))<br>              (<span class="hljs-name">overlay/align</span> <span class="hljs-string">&quot;center&quot;</span> <span class="hljs-string">&quot;bottom&quot;</span><br>                             (<span class="hljs-name">rotate</span> <span class="hljs-number">90</span> (<span class="hljs-name">text</span> <span class="hljs-string">&quot;S1&quot;</span> FONT-SIZE FONT-COLOR))<br>                             (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">8000</span> Y-SCALE) <span class="hljs-string">&quot;outline&quot;</span> <span class="hljs-string">&quot;black&quot;</span>)<br>                             (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">8000</span> Y-SCALE) <span class="hljs-string">&quot;solid&quot;</span> BAR-COLOR)))<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">make-bar</span> s)<br>        (<span class="hljs-name">overlay/align</span> <span class="hljs-string">&quot;center&quot;</span> <span class="hljs-string">&quot;bottom&quot;</span><br>                       (<span class="hljs-name">rotate</span> <span class="hljs-number">90</span> (<span class="hljs-name">text</span> (<span class="hljs-name">school-name</span> s) FONT-SIZE FONT-COLOR))<br>                       (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> (<span class="hljs-name">school-tuition</span> s) Y-SCALE) <span class="hljs-string">&quot;outline&quot;</span> <span class="hljs-string">&quot;black&quot;</span>)<br>                       (<span class="hljs-name">rectangle</span> BAR-WIDTH (<span class="hljs-name"><span class="hljs-built_in">*</span></span> (<span class="hljs-name">school-tuition</span> s) Y-SCALE) <span class="hljs-string">&quot;solid&quot;</span> BAR-COLOR)))<br></code></pre></td></tr></table></figure><h2 id="practice-problems"><a class="markdownIt-Anchor" href="#practice-problems"></a> Practice Problems</h2><p>这一章的 Recommended Problems:</p><ul><li>Reference P1 - Alternative Tuition Graph<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/alternative-tuition-graph-starter.rkt">alternative-tuition-graph-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/alternative-tuition-graph-solution.rkt">alternative-tuition-graph-solution.rkt</a></li></ul></li><li>Reference P2 - Spinning Bears<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/spinning-bears-starter.rkt">spinning-bears-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/spinning-bears-solution.rkt">spinning-bears-solution.rkt</a></li></ul></li></ul><p><em>Reference P1 - Alternative Tuition Graph 这道题是上述的简单变种，有兴趣可以自行尝试</em></p><div class="tag-plugin colorful folders" ><details class="folder" index="0"><summary><p>Reference P2 - Spinning Bears 题解</p></summary><div class="body"><p><strong>预计耗时：100 min / 困难</strong></p><p>这道题确实很难，但我们可以将这个大问题分而治之。看完题目后，我们无非需要做这些事：</p><ul><li>窗口属性、空白背景和熊的属性这些基本信息</li><li>一只熊的数据定义，以及一堆熊的列表</li><li>含有<code>big-bang</code>的<code>main</code>函数，需要<code>on-tick</code>、<code>to-draw</code>，也需要响应鼠标事件的<code>handle-mouse</code></li><li><code>big-bang</code>对应的三个函数</li></ul><p>我们可以挨个处理。</p><p>首先是一些常量，这道题本质上是个 HtDW 设计题，窗口宽高和空白背景是必须的。同时熊的图像定义和旋转速度也是要的。</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> WIDTH <span class="hljs-number">600</span>) <span class="hljs-comment">; width of the scene</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> HEIGHT <span class="hljs-number">700</span>) <span class="hljs-comment">; height of the scene</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> SPEED <span class="hljs-number">3</span>)  <span class="hljs-comment">; speed of rotation</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> MTS (<span class="hljs-name">empty-scene</span> WIDTH HEIGHT)) <span class="hljs-comment">; the empty scene</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> BEAR-IMG &lt;熊的图像&gt;)<br></code></pre></td></tr></table></figure><p>接下来就是数据定义部分。可能一开始思考的时候觉得熊就是一只单纯的熊，但实际上我们需要记录每只熊的位置和方向信息。</p><p>这是因为题目需要我们达到：每点击窗口的某个地方，就在鼠标<strong>落下的位置</strong>出现一只<strong>一直旋转</strong>的熊。</p><p>那么这个<code>bear</code>就存在<strong>x坐标</strong>、<strong>y坐标</strong>和<strong>角度</strong>，它们都是有对应范围的。考虑到鼠标的点击位置并不一定是整数，而角度是保证的，我们可以总结如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">define-struct</span> bear (<span class="hljs-name">x</span> y r))<br><span class="hljs-comment">;; Bear is (make-bear Number[0,WIDTH] Number[0,HEIGHT] Integer)</span><br><span class="hljs-comment">;; interp.  (make-bear x y r) is the state of a bear, where</span><br><span class="hljs-comment">;;  x is the x coordinate in pixels,</span><br><span class="hljs-comment">;;  y is the y coordinate in pixels, and</span><br><span class="hljs-comment">;;  r is the angle of rotation in degrees</span><br></code></pre></td></tr></table></figure><p>与此同时写上它的例子：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> B1 (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span>)) <span class="hljs-comment">; bear in the upper left corner</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> B2 (<span class="hljs-name">make-bear</span> (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>) <span class="hljs-number">90</span>)) <span class="hljs-comment">; sideways bear in the middle</span><br></code></pre></td></tr></table></figure><p>作为一个结构，我们需要在函数模板中提到它所有的字段：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs scheme">#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-bear</span> b)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">bear-x</span> b)     <span class="hljs-comment">; Number[0,WIDTH]</span><br>       (<span class="hljs-name">bear-y</span> b)     <span class="hljs-comment">; Number[0,HEIGHT]</span><br>       (<span class="hljs-name">bear-r</span> b)))   <span class="hljs-comment">; Integer</span><br><br><span class="hljs-comment">;; Template Rules Used:</span><br><span class="hljs-comment">;; - compound: 3 fields</span><br></code></pre></td></tr></table></figure><p>以上就是对一只熊的定义，接下来就是一堆熊，称其为<code>ListOfBear</code>。</p><p>回忆本章的定义过程，可以照样子写出如下代码：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfBear is one of:</span><br><span class="hljs-comment">;; - empty</span><br><span class="hljs-comment">;; - (cons Bear ListOfBear)</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LB0 empty)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LB1 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> B1 empty))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LB2 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> B1 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> B2 empty)))<br><br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-lob</span> lob)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> lob) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> <br>         (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">fn-for-bear</span> (<span class="hljs-name">first</span> lob))<br>              (<span class="hljs-name">fn-for-lob</span> (<span class="hljs-name">rest</span> lob)))]))<br><br><span class="hljs-comment">;; Template Rules Used:</span><br><span class="hljs-comment">;; - one of: 2 cases</span><br><span class="hljs-comment">;; - atomic distinct: empty</span><br><span class="hljs-comment">;; - compound: 2 fields</span><br><span class="hljs-comment">;; - reference: (first lob) is Bear</span><br><span class="hljs-comment">;; - self-reference: (rest lob) is ListOfBear</span><br></code></pre></td></tr></table></figure><hr /><p>数据都准备好了，就剩下处理它们的函数了。首先是<code>main</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfBear -&gt; ListOfBear</span><br><span class="hljs-comment">;; start the world with (main empty)</span><br><span class="hljs-comment">;; </span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">main</span> lob)<br>  (<span class="hljs-name">big-bang</span> lob                         <span class="hljs-comment">; ListOfBear</span><br>            (<span class="hljs-name">on-tick</span>   spin-bears)      <span class="hljs-comment">; ListOfBear -&gt; ListOfBear</span><br>            (<span class="hljs-name">to-draw</span>   render-bears)    <span class="hljs-comment">; ListOfBear -&gt; Image</span><br>            (<span class="hljs-name">on-mouse</span>  handle-mouse)))  <span class="hljs-comment">; ListOfBear Integer Integer MouseEvent -&gt; ListOfBear</span><br></code></pre></td></tr></table></figure><p><em>ps: 有关<code>on-mouse</code>的参数可在 Help Desk 寻找</em></p><p>第一个就是处理每只熊旋转的函数，即<code>spin-bears</code>。它的职责很简单，就是让熊列表中的每一项转一下，测试如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfBear -&gt; ListOfBear</span><br><span class="hljs-comment">;; spin all of the bears forward by SPEED degrees</span><br><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">spin-bears</span> empty) empty)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">spin-bears</span> <br>               (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span>) empty))<br>              (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">0</span> SPEED)) empty))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">spin-bears</span> <br>               (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span>)<br>                     (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>) <span class="hljs-number">90</span>) <br>                           empty)))<br>              (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">0</span> SPEED))<br>                    (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">90</span> SPEED)) empty)))               <br></code></pre></td></tr></table></figure><p>桩函数为<code>;(define (spin-bears lob) empty)</code></p><p>从<code>ListOfBear</code>的函数模板复制过来，改名为<code>spin-bears</code>。回顾这个函数的签名<code>ListOfBear -&gt; ListOfBear</code>，这个函数最终产出的东西也得是熊列表。这就意味着我们在<strong>遍历</strong>这个列表的同时，要顺便修改其中每一项的角度。这里我们将修改角度交给<strong>封装</strong>后的函数<code>spin-bear</code>，这样就简洁一些：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Took template from ListOfBear</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">spin-bears</span> lob)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> lob) empty]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">spin-bear</span> (<span class="hljs-name">first</span> lob))<br>               (<span class="hljs-name">spin-bears</span> (<span class="hljs-name">rest</span> lob)))]))<br></code></pre></td></tr></table></figure><p>对于修改一只熊的朝向，实现方式就是创建一只位置一样但是角度偏向的熊，其签名是<code>Bear -&gt; Bear</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Bear -&gt; Bear</span><br><span class="hljs-comment">;; spin a bear forward by SPEED degrees</span><br><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">spin-bear</span> (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span>)) (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">0</span> SPEED)))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">spin-bear</span> (<span class="hljs-name">make-bear</span> (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>) <span class="hljs-number">90</span>)) <br>              (<span class="hljs-name">make-bear</span> (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">90</span> SPEED)))<br><br><span class="hljs-comment">;(define (spin-bear b) b)</span><br><br><span class="hljs-comment">;; Took template from Bear</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">spin-bear</span> b)<br>  (<span class="hljs-name">make-bear</span> (<span class="hljs-name">bear-x</span> b)<br>             (<span class="hljs-name">bear-y</span> b)<br>             (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name">bear-r</span> b) SPEED)))<br></code></pre></td></tr></table></figure><p>运算的事情处理完了，第二件事就是将转好的熊放到场景内，即<code>render-bears</code>函数。和<code>spin-bears</code>一样，我们可以让显示每只熊的详细操作<strong>封装</strong>给<code>render-bear-on</code>函数，这个函数也会递归着显示每只熊：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfBear -&gt; Image</span><br><span class="hljs-comment">;; render the bears onto the empty scene</span><br><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-bears</span> empty) MTS)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-bears</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span>) empty))<br>              (<span class="hljs-name">place-image</span> (<span class="hljs-name">rotate</span> <span class="hljs-number">0</span> BEAR-IMG) <span class="hljs-number">0</span> <span class="hljs-number">0</span> MTS))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-bears</span> <br>               (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span>)<br>                     (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>) <span class="hljs-number">90</span>) <br>                           empty)))<br>              (<span class="hljs-name">place-image</span> (<span class="hljs-name">rotate</span> <span class="hljs-number">0</span> BEAR-IMG) <span class="hljs-number">0</span> <span class="hljs-number">0</span> <br>                           (<span class="hljs-name">place-image</span> (<span class="hljs-name">rotate</span> <span class="hljs-number">90</span> BEAR-IMG) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>)<br>                                        MTS)))               <br><br><br><span class="hljs-comment">;(define (render-bears lob) MTS)</span><br><br><span class="hljs-comment">;; Took Template from ListOfBear</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render-bears</span> lob)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> lob) MTS]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> <br>         (<span class="hljs-name">render-bear-on</span> (<span class="hljs-name">first</span> lob) (<span class="hljs-name">render-bears</span> (<span class="hljs-name">rest</span> lob)))]))<br></code></pre></td></tr></table></figure><p>对于显示每只熊，也是用<code>place-image</code>，但需要注意的是，熊的角度有可能超过<code>360</code>度，所以我们需要用<code>modulo</code>来处理一下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Bear Image -&gt; Image</span><br><span class="hljs-comment">;; render an image of the bear on the given image</span><br><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-bear-on</span> (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span>) MTS) (<span class="hljs-name">place-image</span> (<span class="hljs-name">rotate</span> <span class="hljs-number">0</span> BEAR-IMG) <span class="hljs-number">0</span> <span class="hljs-number">0</span> MTS))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-bear-on</span> (<span class="hljs-name">make-bear</span> (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>) <span class="hljs-number">90</span>) MTS)<br>              (<span class="hljs-name">place-image</span> (<span class="hljs-name">rotate</span> <span class="hljs-number">90</span> BEAR-IMG) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>) MTS))<br><br><span class="hljs-comment">;(define (render-bear-on b img) MTS)</span><br><br><span class="hljs-comment">;; Took Template from Bear w/ added atomic parameter</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render-bear-on</span> b img)<br>  (<span class="hljs-name">place-image</span> (<span class="hljs-name">rotate</span> (<span class="hljs-name"><span class="hljs-built_in">modulo</span></span> (<span class="hljs-name">bear-r</span> b) <span class="hljs-number">360</span>) BEAR-IMG) (<span class="hljs-name">bear-x</span> b) (<span class="hljs-name">bear-y</span> b) img))<br></code></pre></td></tr></table></figure><p>最后就是处理鼠标点击的函数<code>handle-mouse</code>，它的签名是<code>ListOfBear Integer Integer MouseEvent -&gt; ListOfBear</code>，目的是<code>On mouse-click, adds a bear with 0 rotation to the list at the x, y location</code>。</p><p>查阅文档后知道我们只需要响应<code>&quot;button-down&quot;</code>事件即可（借助<code>(mouse=? me &quot;button-down&quot;)</code>）。鼠标点击时，<code>x</code>和<code>y</code>是鼠标的坐标，而我们需要在这个位置添加一只熊，所以可以直接使用<code>make-bear</code>来创建一只熊，然后将其添加到列表中。</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfBear Integer Integer MouseEvent -&gt; ListOfBear</span><br><span class="hljs-comment">;; On mouse-click, adds a bear with 0 rotation to the list at the x, y location</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">handle-mouse</span> empty <span class="hljs-number">5</span> <span class="hljs-number">4</span> <span class="hljs-string">&quot;button-down&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> <span class="hljs-number">5</span> <span class="hljs-number">4</span> <span class="hljs-number">0</span>) empty))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">handle-mouse</span> empty <span class="hljs-number">5</span> <span class="hljs-number">4</span> <span class="hljs-string">&quot;move&quot;</span>) empty)<br><br><span class="hljs-comment">;(define (handle-mouse lob x y mev) empty)</span><br><br><br><span class="hljs-comment">;; Templated according to MouseEvent large enumeration.</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">handle-mouse</span> lob x y mev)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">mouse=?</span> mev <span class="hljs-string">&quot;button-down&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> x y <span class="hljs-number">0</span>) lob)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> lob]))<br></code></pre></td></tr></table></figure><p>最后运行通过，在交互区输入<code>(main empty)</code>，然后点击窗口的任意位置，就会出现一只熊并开始旋转。</p><p>最终代码如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Spinning Bears</span><br><br><span class="hljs-comment">;; =================</span><br><span class="hljs-comment">;; Constants:</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> WIDTH <span class="hljs-number">600</span>) <span class="hljs-comment">; width of the scene</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> HEIGHT <span class="hljs-number">700</span>) <span class="hljs-comment">; height of the scene</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> SPEED <span class="hljs-number">3</span>)  <span class="hljs-comment">; speed of rotation</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> MTS (<span class="hljs-name">empty-scene</span> WIDTH HEIGHT)) <span class="hljs-comment">; the empty scene</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> BEAR-IMG .)<br><br><span class="hljs-comment">;; =================</span><br><span class="hljs-comment">;; Data definitions:</span><br><br>(<span class="hljs-name">define-struct</span> bear (<span class="hljs-name">x</span> y r))<br><span class="hljs-comment">;; Bear is (make-bear Number[0,WIDTH] Number[0,HEIGHT] Integer)</span><br><span class="hljs-comment">;; interp.  (make-bear x y r) is the state of a bear, where</span><br><span class="hljs-comment">;;  x is the x coordinate in pixels,</span><br><span class="hljs-comment">;;  y is the y coordinate in pixels, and</span><br><span class="hljs-comment">;;  r is the angle of rotation in degrees</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> B1 (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span>)) <span class="hljs-comment">; bear in the upper left corner</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> B2 (<span class="hljs-name">make-bear</span> (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>) <span class="hljs-number">90</span>)) <span class="hljs-comment">; sideways bear in the middle</span><br><br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-bear</span> b)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">bear-x</span> b)     <span class="hljs-comment">; Number[0,WIDTH]</span><br>       (<span class="hljs-name">bear-y</span> b)     <span class="hljs-comment">; Number[0,HEIGHT]</span><br>       (<span class="hljs-name">bear-r</span> b)))   <span class="hljs-comment">; Integer</span><br><br><span class="hljs-comment">;; Template Rules Used:</span><br><span class="hljs-comment">;; - compound: 3 fields</span><br><br><br><span class="hljs-comment">;; ListOfBear is one of:</span><br><span class="hljs-comment">;; - empty</span><br><span class="hljs-comment">;; - (cons Bear ListOfBear)</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LB0 empty)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LB1 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> B1 empty))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LB2 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> B1 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> B2 empty)))<br><br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-lob</span> lob)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> lob) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> <br>         (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">fn-for-bear</span> (<span class="hljs-name">first</span> lob))<br>              (<span class="hljs-name">fn-for-lob</span> (<span class="hljs-name">rest</span> lob)))]))<br><br><span class="hljs-comment">;; Template Rules Used:</span><br><span class="hljs-comment">;; - one of: 2 cases</span><br><span class="hljs-comment">;; - atomic distinct: empty</span><br><span class="hljs-comment">;; - compound: 2 fields</span><br><span class="hljs-comment">;; - reference: (first lob) is Bear</span><br><span class="hljs-comment">;; - self-reference: (rest lob) is ListOfBear</span><br><br><span class="hljs-comment">;; =================</span><br><span class="hljs-comment">;; Functions:</span><br><br><span class="hljs-comment">;; ListOfBear -&gt; ListOfBear</span><br><span class="hljs-comment">;; start the world with (main empty)</span><br><span class="hljs-comment">;; </span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">main</span> lob)<br>  (<span class="hljs-name">big-bang</span> lob                         <span class="hljs-comment">; ListOfBear</span><br>            (<span class="hljs-name">on-tick</span>   spin-bears)      <span class="hljs-comment">; ListOfBear -&gt; ListOfBear</span><br>            (<span class="hljs-name">to-draw</span>   render-bears)    <span class="hljs-comment">; ListOfBear -&gt; Image</span><br>            (<span class="hljs-name">on-mouse</span>  handle-mouse)))  <span class="hljs-comment">; ListOfBear Integer Integer MouseEvent -&gt; ListOfBear</span><br><br><span class="hljs-comment">;; ListOfBear -&gt; ListOfBear</span><br><span class="hljs-comment">;; spin all of the bears forward by SPEED degrees</span><br><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">spin-bears</span> empty) empty)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">spin-bears</span> <br>               (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span>) empty))<br>              (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">0</span> SPEED)) empty))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">spin-bears</span> <br>               (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span>)<br>                     (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>) <span class="hljs-number">90</span>) <br>                           empty)))<br>              (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">0</span> SPEED))<br>                    (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">90</span> SPEED)) empty)))               <br><br><span class="hljs-comment">;(define (spin-bears lob) empty)</span><br><br><span class="hljs-comment">;; Took template from ListOfBear</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">spin-bears</span> lob)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> lob) empty]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">spin-bear</span> (<span class="hljs-name">first</span> lob))<br>               (<span class="hljs-name">spin-bears</span> (<span class="hljs-name">rest</span> lob)))]))<br><br><br><span class="hljs-comment">;; Bear -&gt; Bear</span><br><span class="hljs-comment">;; spin a bear forward by SPEED degrees</span><br><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">spin-bear</span> (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span>)) (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">0</span> SPEED)))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">spin-bear</span> (<span class="hljs-name">make-bear</span> (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>) <span class="hljs-number">90</span>)) <br>              (<span class="hljs-name">make-bear</span> (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">90</span> SPEED)))<br><br><span class="hljs-comment">;(define (spin-bear b) b)</span><br><br><span class="hljs-comment">;; Took template from Bear</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">spin-bear</span> b)<br>  (<span class="hljs-name">make-bear</span> (<span class="hljs-name">bear-x</span> b)<br>             (<span class="hljs-name">bear-y</span> b)<br>             (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name">bear-r</span> b) SPEED)))<br><br><br><span class="hljs-comment">;; ListOfBear -&gt; Image</span><br><span class="hljs-comment">;; render the bears onto the empty scene</span><br><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-bears</span> empty) MTS)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-bears</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span>) empty))<br>              (<span class="hljs-name">place-image</span> (<span class="hljs-name">rotate</span> <span class="hljs-number">0</span> BEAR-IMG) <span class="hljs-number">0</span> <span class="hljs-number">0</span> MTS))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-bears</span> <br>               (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span>)<br>                     (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>) <span class="hljs-number">90</span>) <br>                           empty)))<br>              (<span class="hljs-name">place-image</span> (<span class="hljs-name">rotate</span> <span class="hljs-number">0</span> BEAR-IMG) <span class="hljs-number">0</span> <span class="hljs-number">0</span> <br>                           (<span class="hljs-name">place-image</span> (<span class="hljs-name">rotate</span> <span class="hljs-number">90</span> BEAR-IMG) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>)<br>                                        MTS)))               <br><br><br><span class="hljs-comment">;(define (render-bears lob) MTS)</span><br><br><span class="hljs-comment">;; Took Template from ListOfBear</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render-bears</span> lob)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> lob) MTS]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> <br>         (<span class="hljs-name">render-bear-on</span> (<span class="hljs-name">first</span> lob) (<span class="hljs-name">render-bears</span> (<span class="hljs-name">rest</span> lob)))]))<br><br><br><span class="hljs-comment">;; Bear Image -&gt; Image</span><br><span class="hljs-comment">;; render an image of the bear on the given image</span><br><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-bear-on</span> (<span class="hljs-name">make-bear</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span>) MTS) (<span class="hljs-name">place-image</span> (<span class="hljs-name">rotate</span> <span class="hljs-number">0</span> BEAR-IMG) <span class="hljs-number">0</span> <span class="hljs-number">0</span> MTS))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-bear-on</span> (<span class="hljs-name">make-bear</span> (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>) <span class="hljs-number">90</span>) MTS)<br>              (<span class="hljs-name">place-image</span> (<span class="hljs-name">rotate</span> <span class="hljs-number">90</span> BEAR-IMG) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>) (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>) MTS))<br><br><span class="hljs-comment">;(define (render-bear-on b img) MTS)</span><br><br><span class="hljs-comment">;; Took Template from Bear w/ added atomic parameter</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render-bear-on</span> b img)<br>  (<span class="hljs-name">place-image</span> (<span class="hljs-name">rotate</span> (<span class="hljs-name"><span class="hljs-built_in">modulo</span></span> (<span class="hljs-name">bear-r</span> b) <span class="hljs-number">360</span>) BEAR-IMG) (<span class="hljs-name">bear-x</span> b) (<span class="hljs-name">bear-y</span> b) img))<br><br><br><br><span class="hljs-comment">;; ListOfBear Integer Integer MouseEvent -&gt; ListOfBear</span><br><span class="hljs-comment">;; On mouse-click, adds a bear with 0 rotation to the list at the x, y location</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">handle-mouse</span> empty <span class="hljs-number">5</span> <span class="hljs-number">4</span> <span class="hljs-string">&quot;button-down&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> <span class="hljs-number">5</span> <span class="hljs-number">4</span> <span class="hljs-number">0</span>) empty))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">handle-mouse</span> empty <span class="hljs-number">5</span> <span class="hljs-number">4</span> <span class="hljs-string">&quot;move&quot;</span>) empty)<br><br><span class="hljs-comment">;(define (handle-mouse lob x y mev) empty)</span><br><br><br><span class="hljs-comment">;; Templated according to MouseEvent large enumeration.</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">handle-mouse</span> lob x y mev)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">mouse=?</span> mev <span class="hljs-string">&quot;button-down&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">make-bear</span> x y <span class="hljs-number">0</span>) lob)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> lob]))<br></code></pre></td></tr></table></figure></div></details></div>]]>
    </content>
    <id>https://ziling.moe/2025/academics-ubc-cpsc-110-reference/</id>
    <link href="https://ziling.moe/2025/academics-ubc-cpsc-110-reference/"/>
    <published>2025-08-02T07:50:00.000Z</published>
    <summary>UBC 的计科大一必修课 - CPSC 110</summary>
    <title>UBC - CPSC 110 - Reference</title>
    <updated>2025-08-02T07:50:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term="Academics" scheme="https://ziling.moe/categories/Academics/"/>
    <category term="UBC" scheme="https://ziling.moe/tags/UBC/"/>
    <category term="计算机科学" scheme="https://ziling.moe/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    <category term="教程" scheme="https://ziling.moe/tags/%E6%95%99%E7%A8%8B/"/>
    <content>
      <![CDATA[<p>我们在之前的学习中控制了猫和牛，但都是单个的。不仅如此，之前我们的数据定义整体上都是<strong>单个</strong>的。那么我们该如何控制<strong>多个</strong>数据呢，一些城市、一群牛？</p><p>这些信息被称为<strong>任意规模信息</strong>，也就是事先无法确定其大小的信息，也是现实中最容易遇到的情况。</p><p><em>ps: 好在本章的代码会比 HtDW 和复合类型简单</em></p><h2 id="学习目标"><a class="markdownIt-Anchor" href="#学习目标"></a> 学习目标</h2><ul><li>能够运用列表机制来构建和解析列表结构</li><li>能够识别需要采用列表及结构体列表来表示的、规模不定的问题领域信息</li><li>能能够使用 HtDD、HtDF 和数据驱动模板配方处理此类数据</li><li>能够解释何为结构良好的自引用数据定义，并判断特定自引用数据定义是否符合良好结构要求</li><li>能够设计出处理并生成列表及结构体列表的函数</li><li>能够预测并识别数据定义中的自引用与操作该数据的函数中自然递归之间的对应关系</li></ul><mark class="tag-plugin colorful mark" color="warning">以下内容涉及到的edX链接均不保证可访问性</mark><h2 id="introduction-to-arbitrary-sized-data"><a class="markdownIt-Anchor" href="#introduction-to-arbitrary-sized-data"></a> Introduction to Arbitrary Sized Data</h2><p>我们之前学的所有内容，里面的数据结构都是<emp>定长</emp> <em>(Fixed Size)</em> 的，但很多时候信息本身都是不确定的，数据又如何能确定呢？</p><h2 id="list-mechanisms"><a class="markdownIt-Anchor" href="#list-mechanisms"></a> List Mechanisms</h2><p>本节会介绍<emp>列表</emp> <em>(List)</em> 这个数据结构。顾名思义，它会存储一串某个类型的值。</p><p>我们可以在 DrRacket 里面写<code>empty</code>，它的意思也很顾名思义，就是空的列表（可以是任何类型）。但更准确地说，它通常被用于<strong>占位</strong>，因为<code>cons</code>表达式必须接受两个值，第二个值如果不存在就只能写个<code>empty</code>。</p><p>那么对于真正有数据的列表，我们可以使用<code>cons</code>表达式来构建，比如以下定义，之后可以运行下看看是什么输出：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;Flames&quot;</span> empty)                 <span class="hljs-comment">; a list of 1 element</span><br>(<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;Leafs&quot;</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;Flames&quot;</span> empty))  <span class="hljs-comment">; a list of 2 elements</span><br></code></pre></td></tr></table></figure><p>第一个<code>cons</code>构建了由一个元素组成的列表，这个元素的类型是<code>String</code>，后面的<code>empty</code>是空列表，它不计入元素数量。</p><p>第二个<code>cons</code>是由一个<code>String</code>和一个列表组成的列表，后面的列表里头虽说有<code>empty</code>，但整体上仍是由一个元素组成的列表，故它是两个元素的列表。</p><p>再试试这一句<code>(cons (string-append &quot;C&quot; &quot;anucks&quot;) empty)</code>会得到什么：<code>(cons &quot;Canucks&quot; '())</code>。</p><p>这是因为在构建列表的时候，它会尝试<strong>将里面所有的表达式都变成确切的值</strong>。也就是说<code>string-append</code>会被执行，变成值然后替换表达式。同理，也可以试试<code>(cons (square 10 &quot;solid&quot; &quot;blue&quot;) (cons (triangle 20 &quot;solid&quot; &quot;green&quot;) empty))</code>。</p><p>如果接触过其他编程语言的列表，可能会觉得<code>(cons 10 (cons 9 (cons 10 empty)))</code>是两个元素，但实际上在 Racket 这里是三个，它不是算最外层元素数量的。</p><p>把代码简化为：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">require</span></span> <span class="hljs-number">2</span>htdp/image)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> L1 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;Flames&quot;</span> empty))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> L2 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">10</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">9</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">10</span> empty))))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> L3 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">square</span> <span class="hljs-number">10</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">triangle</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;green&quot;</span>) empty)))<br></code></pre></td></tr></table></figure><p>为了防止混淆，这并不是在嵌套，上面的三个<code>cons</code>实际上会得到：</p><ul><li><code>&quot;Flames&quot;</code>和<code>'()</code></li><li><code>10</code>、<code>9</code>、<code>10</code>和<code>'()</code></li><li><code>(square 10 &quot;solid&quot; &quot;blue&quot;)的图像</code>、<code>(triangle 20 &quot;solid&quot; &quot;green&quot;)的图像</code>和<code>'()</code></li></ul><hr /><p>需要注意的是，<code>cons</code>本身其实就是一种复合类型的构造器，支持传入两个字段来组成列表。这就是有些同学可能会觉得为什么要嵌套声明列表。</p><p>列表的构建说完了，接下来就是它的相关操作。<em>我们在学习数据结构时总是如此</em>。</p><p>先是<code>(first L1)</code>，这一行会得到列表<code>L1</code>的第一个元素，即<code>&quot;Flames&quot;</code>。相对的<code>(rest L1)</code>会返回<code>L1</code>第一个元素之后的所有元素。可以试试将<code>L1</code>换成<code>L2</code>或<code>L3</code>来体验。</p><p>那么我们该如何获取第二个、第三个这种确切位置的元素呢？不同于其他编程语言<em>下标</em>的概念，这里只能通过<code>first</code>和<code>rest</code>表达式凑出来。比如：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">first</span> (<span class="hljs-name">rest</span> L2))         <span class="hljs-comment">; get the second element of L2</span><br>(<span class="hljs-name">first</span> (<span class="hljs-name">rest</span> (<span class="hljs-name">rest</span> L2)))  <span class="hljs-comment">; get the third element of L2</span><br></code></pre></td></tr></table></figure><p><em>ps: 有点怪只能说这个设计</em></p><p>最后就是<code>empty?</code>表达式，这个表达式后面接受一个参数，判断列表是不是<code>empty</code>，比如<code>(empty? empty)</code>的返回结果就是<code>true</code>，而<code>(empty? L1)</code>之类就是<code>false</code>。</p><h2 id="list-data-definition"><a class="markdownIt-Anchor" href="#list-data-definition"></a> List Data Definition</h2><p>本节将会讲述列表的数据结构设计，在下一节探讨它的函数设计。在此之前，<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/quidditch-starter.rkt">下载来自edX的 cat-starter.rkt 文件</a>。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:935/325;width:500px;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-self-reference/quidditch-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-self-reference/quidditch-starter.webp" alt="quidditch-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">quidditch-starter.rkt</span></div></div><p>我们将要设计一个记录你最喜欢的魁地奇 <em>(Quidditch，《哈利·波特》系列中重要的空中团队对抗运动)</em> 队伍，需要先为它设计一个数据定义。</p><p>将光标移动到注释框的下方，然后在 DrRacket 上方工具栏找到<code>Insert</code>，点击展开后选择<code>Insert Comment Box</code>，就能得到一个注释框。然后按照这个操作再加一个。我们先列举下信息：</p><p>首先，有三个队：<code>UBC</code>、<code>McGill</code>和<code>Team Who Must Not be Named</code> <em>(教授注：第三个不是指 UofT)</em>。将这些信息放在第一个框。</p><p>然后就是由信息得到的数据结构，大概是<code>(cons &quot;UBC&quot; (cons &quot;McGill&quot; empty))</code>，第三个就是无名。</p><p>最后汇总如下：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:877/259;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-self-reference/quidditch-starter-info-data.webp" data-src="/images/2025/academics-ubc-cpsc-110-self-reference/quidditch-starter-info-data.webp" alt="Information & Data" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">Information & Data</span></div></div><hr /><p>之后再提一行，在程序的下方真正开始设计数据结构。我们将这个类型称为<code>ListOfString</code>，和枚举相似，它也是<code>one of</code>的，但有个比较有意思的点：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfString is one of:</span><br><span class="hljs-comment">;;   - empty</span><br><span class="hljs-comment">;;   - (cons String ListOfString)</span><br><span class="hljs-comment">;; interp. a list of strings</span><br></code></pre></td></tr></table></figure><p>有没有发现这个数据定义的第二个情况出现了它自己，这被称为<emp>自我引用</emp> <em>(Self-reference)</em>。尝试自行梳理下为什么会这么写，可以看看它的例子是什么：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LOS1 empty)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LOS2 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;McGill&quot;</span> empty))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LOS3 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;UBC&quot;</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;McGill&quot;</span> empty)))<br></code></pre></td></tr></table></figure><details class="tag-plugin colorful folding" color="blue" open><summary><p>为什么这么写？</p></summary><div class="body"><p>为什么第二个不是写成<code>(cons ListOfString String)</code>呢？这是因为列表是有顺序的，第一个元素是<code>String</code>，后面跟着的是<code>ListOfString</code>，创建列表的时候会遵循这个顺序。</p> </div></details><p>每个例子都符合该数据定义，<code>LOS1</code>直接符合第一种情况，而<code>LOS2</code>就先符合第二种情况，然后内部又符合第一种，<code>LOS3</code>同理。</p><p>以<code>LOS2</code>为例，它是怎么符合的：</p><ul><li>首先是<code>(cons &quot;McGill&quot; empty)</code>，根据定义，它很像第二种情况，但我们不确定。</li><li>观察内部结构，存在一个<code>String</code>和一个<code>empty</code>，我们知道<code>ListOfString</code>确实有<code>empty</code>的情况。</li><li>那么它的结构实际上可以是<code>(cons String ListOfString)</code>，故满足。</li></ul><p>会发现这个<code>ListOfString</code>有可能会呈现无限嵌套的情况，每个<code>ListOfString</code>类型都有可能为空，或者是由<code>ListOfString</code>组成的。这种感觉也可以被称之为<emp>递归</emp> <em>(Recursion)</em>，之后的函数设计也会触及这一概念。</p><p>接下来就是函数模板设计，这里不需要那么绕，因为<code>one of</code>下面只有两种情况，那就在<code>cond</code>表达式写两个<code>Q A</code>就行：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-los</span> los)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [<span class="hljs-name">Q</span> A]<br>        [<span class="hljs-name">Q</span> A]))<br></code></pre></td></tr></table></figure><p>唯一需要注意的是模板规则，<code>empty</code>其实是<code>atomic distinct</code>，而第二种情况就是<code>cons</code>表达式返回的值的类型，即<code>compound</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;   - one of: 2 cases</span><br><span class="hljs-comment">;;   - atomic distinct: empty</span><br><span class="hljs-comment">;;   - compound: (cons String ListOfString)</span><br></code></pre></td></tr></table></figure><p>借此，我们就知道如何完善刚刚的模板。第一个<code>Q</code>实际上是在判断传来的<code>los</code>是不是<code>empty</code>，而第二个<code>Q</code>其实就是<code>else</code>的情况：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-los</span> los)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> los) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">first</span> los)   <span class="hljs-comment">; String</span><br>                   (<span class="hljs-name">rest</span> los))]  <span class="hljs-comment">; ListOfString</span><br>        ))<br></code></pre></td></tr></table></figure><p>数据定义部分代码如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfString is one of:</span><br><span class="hljs-comment">;;   - empty</span><br><span class="hljs-comment">;;   - (cons String ListOfString)</span><br><span class="hljs-comment">;; interp. a list of strings</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LOS1 empty)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LOS2 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;McGill&quot;</span> empty))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LOS3 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;UBC&quot;</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;McGill&quot;</span> empty)))<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-los</span> los)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> los) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">first</span> los)   <span class="hljs-comment">; String</span><br>                   (<span class="hljs-name">rest</span> los))]  <span class="hljs-comment">; ListOfString</span><br>        ))<br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;   - one of: 2 cases</span><br><span class="hljs-comment">;;   - atomic distinct: empty</span><br><span class="hljs-comment">;;   - compound: (cons String ListOfString)</span><br></code></pre></td></tr></table></figure><h2 id="function-operating-on-list"><a class="markdownIt-Anchor" href="#function-operating-on-list"></a> Function Operating on List</h2><p>本节会从上一节最后写的数据定义开始，为这个列表设计函数。问题如下：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1010/233;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-self-reference/quidditch-starter-problem.webp" data-src="/images/2025/academics-ubc-cpsc-110-self-reference/quidditch-starter-problem.webp" alt="Problem" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">Problem</span></div></div><p>如果传入的<code>ListOfString</code>含<code>&quot;UBC&quot;</code>，那么就返回<code>true</code>，那么称这个函数为<code>contains-ubc?</code>吧。</p><p>由题可知，它的函数签名是<code>ListOfString -&gt; Boolean</code>，目的是<code>produce true if los includes &quot;UBC&quot;</code>。然后就是它的测试，我们可以参考<code>ListOfString</code>这个数据结构的例子来写：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfString -&gt; Boolean</span><br><span class="hljs-comment">;; produce true if los includes &quot;UBC&quot;</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">contains-ubc?</span> empty) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">contains-ubc?</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;McGill&quot;</span> empty)) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">contains-ubc?</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;UBC&quot;</span> empty)) true)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">contains-ubc?</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;UBC&quot;</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;McGill&quot;</span> empty))) true)<br></code></pre></td></tr></table></figure><p>它的桩函数是<code>(define (contains-ubc? los) false)  ; stub</code>，也可以此测试下程序现在能不能跑。</p><p>从<code>ListOfString</code>的函数模板复制过来，函数名改为<code>contains-ubc?</code>，开始编写函数体：</p><ul><li>对于<code>empty</code>的情况，我们应该直接返回<code>false</code>，因为空列表不可能含有<code>&quot;UBC&quot;</code></li><li>对于有其他元素的情况，我们需要仔细思考下该怎么写：<ul><li>如果<code>(first los)</code>就是<code>&quot;UBC&quot;</code>，意味着这个列表确实存在<code>&quot;UBC&quot;</code>，那么就返回<code>true</code>。</li><li>但是如果第一个元素不是，该怎么考察剩下的元素有没有<code>&quot;UBC&quot;</code>呢？</li></ul></li></ul><p>这将是本节最绕的地方，可能对于*剩下的元素有没有<code>&quot;UBC&quot;</code>*这个问题暂时摸不着头脑。但是我们可以看看当前函数的签名，你会发现就是<code>ListOfString -&gt; Boolean</code>。</p><p>这是否意味着，有没有一种可能，<strong>我们可以在<code>contains-ubc?</code>函数调用<code>contains-ubc?</code>？</strong></p><p>因为这个函数的目的就是判断一个<code>ListOfString</code>是否存在<code>&quot;UBC&quot;</code>，那我们可以让函数<strong>自己调用自己</strong>，让我们这么写<code>else</code>的<code>A</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">[<span class="hljs-name"><span class="hljs-built_in">else</span></span> (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> (<span class="hljs-name">first</span> los) <span class="hljs-string">&quot;UBC&quot;</span>)<br>          true<br>          (<span class="hljs-name">contains-ubc?</span> (<span class="hljs-name">rest</span> los)))]<br></code></pre></td></tr></table></figure><p>如果没转过弯，可能会觉得这难道不会一直在判断一个列表是否存在<code>&quot;UBC&quot;</code>呢？不会死在那吗？实则不然。</p><p>仔细观察这个部分，<code>if</code>表达式会判断当前列表的首项是不是<code>&quot;UBC&quot;</code>，如果不是，再将<strong>剩下元素</strong>传给下一个<code>contains-ubc?</code>。也就是说第二个<code>contains-ubc?</code>并没有拿到完整列表，它少了第一项。以此往复，下一个<code>contains-ubc?</code>永远都会比上一个少一项，这样就能判断剩下的元素究竟是<strong>空</strong>还是首项为<code>&quot;UBC&quot;</code>。</p><p>这被称为<emp>递归</emp>，简单理解就是一个函数会<strong>自己调用自己</strong>。</p><p>递归有去有回，它该怎么回来呢？假如<code>(contains-ubc? (rest los))</code>这个表达式被执行了<code>10</code>次，也就是说这个函数自身调用自己了<code>9</code>次，那么：</p><ul><li>当列表本身被穷尽还没找到<code>&quot;UBC&quot;</code>，那函数必然会到达<code>(empty? los)</code>分支，返回<code>false</code></li><li>当列表的首项是<code>&quot;UBC&quot;</code>，它会返回<code>true</code></li></ul><p>这个函数不会无限调用下去，它总是会到达一个终点，终点的布尔值会让这一切回到最初的函数中，返回出来。</p><h2 id="revising-the-recipes-for-lists"><a class="markdownIt-Anchor" href="#revising-the-recipes-for-lists"></a> Revising the Recipes for Lists</h2><p>我们接触到了<code>list</code>的定义和操作，本节将会复盘前两节我们学到的内容。在此之前，<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/quidditch-recap-starter.rkt">下载来自edX的 quidditch-recap-starter.rkt 文件</a>。</p><p><strong>自我引用</strong>在定义中引用自身，像一个数据结构在绕圈子，同时<strong>递归</strong>则是函数调用自身，像一个函数在绕圈子。</p><p>打开代码文件后，我们能从<code>your favorite Quidditch teams</code>发现这个信息包含着<strong>随机</strong> <em>(Arbitrary)</em> 数量的队伍，这也代表着它需要一个<strong>不定</strong>长度的列表来存储。</p><p>往下看看到<code>ListOfString</code>的定义，和之前的<code>ListOfString</code>一样，它也是一个自我引用的数据定义。我们可以看到它的两个情况：</p><ul><li><code>empty</code>，属于<code>base case</code>，表示空列表</li><li><code>(cons String ListOfString)</code>，属于<code>self-reference case</code>，表示一个<code>String</code>和一个<code>ListOfString</code>的组合</li></ul><p>对于测试来说，这两个<code>case</code>都需要覆盖到。</p><p>它在被处理的时候很可能会绕圈，但为什么最终会停下呢？就是因为<code>base case</code>的存在。每次调用<code>contains-ubc?</code>，它都会减少一个元素，直到它到达空列表。</p><p>它的函数模板其实并不完整，让我们重新看下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;   - one of: 2 cases</span><br><span class="hljs-comment">;;   - atomic distinct: empty</span><br><span class="hljs-comment">;;   - compound: (cons String ListOfString)</span><br></code></pre></td></tr></table></figure><p>我们实际上是要<strong>递归地</strong>处理一个列表的，这个模板并没有体现出这一点。我们需要在最后注明它的<code>self-reference</code>，也就是：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;   - one of: 2 cases</span><br><span class="hljs-comment">;;   - atomic distinct: empty</span><br><span class="hljs-comment">;;   - compound: (cons String ListOfString)</span><br><span class="hljs-comment">;;   - self-reference: (rest los) is ListOfString</span><br></code></pre></td></tr></table></figure><hr /><p>再让我们看到函数设计部分。列表的测试也需要覆盖到两个<code>case</code>，同时<code>self-reference case</code>需要依赖<code>base case</code>来终止。</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">contains-ubc?</span> empty) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">contains-ubc?</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;McGill&quot;</span> empty)) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">contains-ubc?</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;UBC&quot;</span> empty)) true)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">contains-ubc?</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;McGill&quot;</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-string">&quot;UBC&quot;</span> empty))) true)<br></code></pre></td></tr></table></figure><p>对于递归函数，可能会有同学不太能梳理过程，以下是过程详解：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1758/542;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-self-reference/quidditch-starter-problem-explain.webp" data-src="/images/2025/academics-ubc-cpsc-110-self-reference/quidditch-starter-problem-explain.webp" alt="Detailed Explain" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">Detailed Explain</span></div></div><h2 id="designing-with-lists-positions-in-list-templates"><a class="markdownIt-Anchor" href="#designing-with-lists-positions-in-list-templates"></a> Designing with Lists &amp; Positions in List Templates</h2><p>我们将从<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/designing-with-lists-1-starter.rkt">来自edX的 designing-with-lists-1-starter.rkt 文件</a>开始：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1012/469;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-self-reference/designing-with-lists-1-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-self-reference/designing-with-lists-1-starter.webp" alt="designing-with-lists-1-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">designing-with-lists-1-starter.rkt</span></div></div><p>我们需要设计一个有关猫头鹰的程序，它需要：</p><ul><li>一个数据定义用来保存所有猫头鹰的重量，称为<code>ListOfNumber</code></li><li>一个函数，接受<code>ListOfNumber</code>，返回所有猫头鹰的总重量</li><li>一个函数，接受<code>ListOfNumber</code>，返回所有猫头鹰的数量</li></ul><p>猫头鹰的数量自然是不确定的，所以它的数据结构需要是一个列表。我们可以先写下数据定义：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfNumber is one of:</span><br><span class="hljs-comment">;; - empty</span><br><span class="hljs-comment">;; - (cons Number ListOfNumber)</span><br><span class="hljs-comment">;; interp. each number in the list is an owl weight in ounces</span><br></code></pre></td></tr></table></figure><p>它的例子需要包含<code>base</code>和<code>self-reference</code>两种情况：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LON1 empty)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LON2 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">60</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">42</span> empty)))<br></code></pre></td></tr></table></figure><p>之后就是函数模板。这次我们需要在模板中形成递归的关系：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-lon</span> lon)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> lon) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">first</span> lon)<br>              (<span class="hljs-name">fn-for-lon</span> (<span class="hljs-name">rest</span> lon)))]))<br></code></pre></td></tr></table></figure><p>以及它的模板规则，记住需要加入<code>self-reference</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;  - one of: 2 cases</span><br><span class="hljs-comment">;;  - atomic distinct: empty</span><br><span class="hljs-comment">;;  - compound: (cons Number ListOfNumber)</span><br><span class="hljs-comment">;;  - self-reference: (rest lon) is ListOfNumber</span><br></code></pre></td></tr></table></figure><hr /><p>接下来就是函数设计部分。让我们再回顾下第二个需求：</p><blockquote><p>一个函数，接受<code>ListOfNumber</code>，返回所有猫头鹰的总重量</p></blockquote><p>我们可以称这个函数为<code>sum</code>，它的签名是<code>ListOfNumber -&gt; Number</code>，目的是<code>produce sum of weights of owls in lon</code>。它的测试需要覆盖到所有情况：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfNumber -&gt; Number</span><br><span class="hljs-comment">;; produce sum of weights of owls in lon</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">sum</span> empty) <span class="hljs-number">0</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">sum</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">60</span> empty)) <span class="hljs-number">60</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">sum</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">60</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">42</span> empty))) (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">60</span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">42</span> <span class="hljs-number">0</span>)))  <span class="hljs-comment">; 格式对应</span><br></code></pre></td></tr></table></figure><p>它的桩函数是<code>;(define (sum lon) 0)  ;stub</code>，也可以此测试下程序现在能不能跑。</p><p>成功后再将函数模板复制过来，函数名改为<code>sum</code>，开始编写函数体：</p><ul><li>对于<code>empty</code>的情况，我们应该直接返回<code>0</code>，因为空列表的总和是<code>0</code></li><li>对于有其他元素的情况：<ul><li>如果<code>(first lon)</code>就是当前猫头鹰的重量，那么我们可以将它加到总和上。</li><li>但是如果有多个猫头鹰，我们需要将<strong>剩下的猫头鹰</strong>的重量也加上。</li></ul></li></ul><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">sum</span> lon)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> lon) <span class="hljs-number">0</span>]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">first</span> lon)          <br>              (<span class="hljs-name">sum</span> (<span class="hljs-name">rest</span> lon)))]))<br></code></pre></td></tr></table></figure><p>我们需要为<code>empty</code>情况返回<code>0</code>，而对于<code>else</code>情况，我们需要将首项的重量加上剩下猫头鹰的总重量，而剩下的重量该如何计算呢？请放心交给下一次递归调用的<code>sum</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">sum</span> lon)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> lon) <span class="hljs-number">0</span>]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name">first</span> lon)           <span class="hljs-comment">; 首项的重量</span><br>                 (<span class="hljs-name">sum</span> (<span class="hljs-name">rest</span> lon)))]))  <span class="hljs-comment">; 剩下猫头鹰的总重量</span><br></code></pre></td></tr></table></figure><hr /><p>第三个需求是：</p><blockquote><p>一个函数，接受<code>ListOfNumber</code>，返回所有猫头鹰的数量</p></blockquote><p>可以称这个函数为<code>count</code>，它的签名是<code>ListOfNumber -&gt; Natural</code>，目的是<code>produce total number of weights in consumed list</code>。它的测试需要覆盖到：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">count</span> empty) <span class="hljs-number">0</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">count</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">12</span> empty)) (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">1</span> <span class="hljs-number">0</span>))                  <span class="hljs-comment">; 1个猫头鹰</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">count</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">35</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">12</span> empty))) (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">1</span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">1</span> <span class="hljs-number">0</span>)))  <span class="hljs-comment">; 2个猫头鹰</span><br></code></pre></td></tr></table></figure><p>桩函数是<code>;(define (count lon) 0)  ; stub</code>，测试下程序能不能跑。</p><p>再将函数模板复制过来，函数名改为<code>count</code>，编写函数体：</p><ul><li>类似于我们刚刚的<code>sum</code>函数，对于<code>empty</code>的情况，我们应该直接返回<code>0</code>，因为空列表的数量是<code>0</code></li><li>对于有其他元素的情况：<ul><li>如果<code>(first lon)</code>是一个猫头鹰的重量，那么我们可以将它计入数量。</li><li>但是如果有多个猫头鹰，我们需要将<strong>剩下的猫头鹰</strong>的数量也加上。</li></ul></li></ul><p>数量的累加和总和不同，它是一个<code>+ 1</code>，因为每次递归调用都会有一个猫头鹰的重量被计入数量：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">count</span> lon)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> lon) <span class="hljs-number">0</span>]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> <br>         (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">1</span> <br>            (<span class="hljs-name">count</span> (<span class="hljs-name">rest</span> lon)))]))<br></code></pre></td></tr></table></figure><p>在思考列表相关的函数设计时，请相信递归能解决问题，自己调用自己不是什么黑箱。</p><h2 id="practice-problems"><a class="markdownIt-Anchor" href="#practice-problems"></a> Practice Problems</h2><p>这一章的 Recommended Problems:</p><ul><li>Self-Reference P2 - Double All<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/double-all-starter.rkt">double-all-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/double-all-solution.rkt">double-all-solution.rkt</a></li></ul></li><li>Self-Reference P3 - Boolean List<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/boolean-list-starter.rkt">boolean-list-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/boolean-list-solution.rkt">boolean-list-solution.rkt</a></li></ul></li><li>Self-Reference P5 - Largest<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/largest-starter.rkt">largest-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/largest-solution.rkt">largest-solution.rkt</a></li></ul></li><li>Self-Reference P6 - Image List<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/image-list-starter.rkt">image-list-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/image-list-solution.rkt">image-list-solution.rkt</a></li></ul></li></ul><div class="tag-plugin colorful folders" ><details class="folder" index="0"><summary><p>Self-Reference P2 - Double All 题解</p></summary><div class="body"><p><strong>预计耗时：18 min / 简单</strong></p><p>这道题的数据定义已经给出了，就是一串数字列表，我们需要得到这些数字相乘后的结果。</p><p>仿照之前我们写的相加，只需要变成相乘就可以，写一些长列表测试即可：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfNumber -&gt; ListOfNumber</span><br><span class="hljs-comment">;; double every number in the given list</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">double-all</span> empty) empty)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">double-all</span> LON2) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span>  <span class="hljs-number">120</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">84</span> empty)))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">double-all</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">10</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">20</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">50</span> empty))))<br>              (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">20</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">40</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">100</span> empty))))<br><br><span class="hljs-comment">;(define (double-all lon) empty) ;stub</span><br><span class="hljs-comment">;&lt;template from ListOfNumber&gt;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">double-all</span> lon)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> lon) empty]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">2</span> (<span class="hljs-name">first</span> lon))<br>               (<span class="hljs-name">double-all</span> (<span class="hljs-name">rest</span> lon)))]))<br></code></pre></td></tr></table></figure></div></details><details class="folder" index="1"><summary><p>Self-Reference P3 - Boolean List 题解</p></summary><div class="body"><p><strong>预计耗时：35 min / 中等</strong></p><p>和上一题同理，数字改布尔，运算从相乘变<code>and</code>就行：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfBoolean is one of:</span><br><span class="hljs-comment">;;  - empty</span><br><span class="hljs-comment">;;  - (cons Boolean ListOfBoolean)</span><br><span class="hljs-comment">;; interp. a list of boolean values</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LOB1 empty)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LOB2 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> true (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> false empty)))<br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-lob</span> lob)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> lob) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">first</span> lob)<br>              (<span class="hljs-name">fn-for-lob</span> (<span class="hljs-name">rest</span> lob)))]))<br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;  - one of: 2 cases</span><br><span class="hljs-comment">;;  - atomic distinct: empty</span><br><span class="hljs-comment">;;  - compound: (cons Boolean ListOfBoolean)</span><br><span class="hljs-comment">;;  - self-reference: (rest lob) is ListOfBoolean</span><br><br><span class="hljs-comment">;; ListOfBoolean -&gt; Boolean</span><br><span class="hljs-comment">;; produces true if all values in the given list are true or if the list is empty</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">all-true?</span> empty) true)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">all-true?</span> LOB2) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">all-true?</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> true (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> true (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> true (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> true empty)))))<br>              true)<br><br><span class="hljs-comment">;(define (all-true? lob) true)  ;stub</span><br><span class="hljs-comment">;&lt;template from ListOfBoolean&gt;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">all-true?</span> lob)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> lob) true]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">and</span></span> (<span class="hljs-name">first</span> lob)<br>              (<span class="hljs-name">all-true?</span> (<span class="hljs-name">rest</span> lob)))]))<br></code></pre></td></tr></table></figure></div></details><details class="folder" index="2"><summary><p>Self-Reference P5 - Largest 题解</p></summary><div class="body"><p><strong>预计耗时：18 min / 简单</strong></p><p>这道题乍一看只是做一些比较，但传入的是一串数字，比较本身只是布尔，所以你需要在判断后将对应的值返回回去，而非判断的布尔本身：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfNumber -&gt; Number</span><br><span class="hljs-comment">;; produce the largest number in the given list, or 0 if empty</span><br><span class="hljs-comment">;; ASSUMES that every element of lon is &gt; 0</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">largest</span> empty) <span class="hljs-number">0</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">largest</span> LON2) <span class="hljs-number">60</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">largest</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">10</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">20</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> <span class="hljs-number">50</span> empty)))) <span class="hljs-number">50</span>)<br><br><span class="hljs-comment">;(define (largest lon) 0) ;stub</span><br><span class="hljs-comment">;&lt;template from ListOfNumber&gt;</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">largest</span> lon)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> lon) <span class="hljs-number">0</span>]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> (<span class="hljs-name">first</span> lon) (<span class="hljs-name">largest</span> (<span class="hljs-name">rest</span> lon)))<br>             (<span class="hljs-name">first</span> lon)<br>             (<span class="hljs-name">largest</span> (<span class="hljs-name">rest</span> lon)))]))<br></code></pre></td></tr></table></figure></div></details><details class="folder" index="3"><summary><p>Self-Reference P6 - Image List 题解</p></summary><div class="body"><p><strong>预计耗时：35 min / 中等</strong></p><p>和第一题相似，只是相加项不明显，我们需要获取图像的面积才行。按照题目提示，图像的面积就是单纯的宽高相乘，我们可以使用<code>image-height</code>和<code>image-width</code>表达式来获取图像的宽高。</p><p>其余就差不多相同了：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; ListOfImage is one of: </span><br><span class="hljs-comment">;;  - empty</span><br><span class="hljs-comment">;;  - (cons Image ListOfImage)</span><br><span class="hljs-comment">;; interp. a list of images</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LI0 empty) <br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LI1 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">circle</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>) empty))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LI2 (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">circle</span> <span class="hljs-number">40</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">circle</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>) empty)))<br><br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-loi</span> loi) <br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> loi) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">first</span> loi)<br>              (<span class="hljs-name">fn-for-loi</span> (<span class="hljs-name">rest</span> loi)))]))<br><br><span class="hljs-comment">;; Template rules used: </span><br><span class="hljs-comment">;; - one of: 2 cases</span><br><span class="hljs-comment">;; - atomic distinct: empty</span><br><span class="hljs-comment">;; - compound: (cons Image ListOfImage)</span><br><span class="hljs-comment">;; - atomic non-distinct: (first loi) is Image</span><br><span class="hljs-comment">;; - self-reference: (rest loi) is ListOfImage</span><br><br><span class="hljs-comment">;; ListOfImage -&gt; Number </span><br><span class="hljs-comment">;; produce total area of all images</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">total-area</span> empty) <span class="hljs-number">0</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">total-area</span> (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">2</span> <span class="hljs-number">4</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)<br>                                (<span class="hljs-name"><span class="hljs-built_in">cons</span></span> (<span class="hljs-name">square</span> <span class="hljs-number">3</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>)<br>                                      empty)))<br>              <span class="hljs-number">17</span>)<br><br><span class="hljs-comment">;(define (total-area loi) 0)  ;stub</span><br><span class="hljs-comment">;&lt;template from ListOfImage&gt;</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">total-area</span> loi) <br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">empty?</span> loi) <span class="hljs-number">0</span>]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name"><span class="hljs-built_in">*</span></span> (<span class="hljs-name">image-width</span> (<span class="hljs-name">first</span> loi))<br>               (<span class="hljs-name">image-height</span> (<span class="hljs-name">first</span> loi)))<br>            (<span class="hljs-name">total-area</span> (<span class="hljs-name">rest</span> loi)))]))<br></code></pre></td></tr></table></figure></div></details></div>]]>
    </content>
    <id>https://ziling.moe/2025/academics-ubc-cpsc-110-self-reference/</id>
    <link href="https://ziling.moe/2025/academics-ubc-cpsc-110-self-reference/"/>
    <published>2025-07-29T03:10:00.000Z</published>
    <summary>UBC 的计科大一必修课 - CPSC 110</summary>
    <title>UBC - CPSC 110 - Self-Reference</title>
    <updated>2025-07-29T03:10:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term="Academics" scheme="https://ziling.moe/categories/Academics/"/>
    <category term="UBC" scheme="https://ziling.moe/tags/UBC/"/>
    <category term="计算机科学" scheme="https://ziling.moe/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    <category term="教程" scheme="https://ziling.moe/tags/%E6%95%99%E7%A8%8B/"/>
    <content>
      <![CDATA[<p>我们在上一章体验了 Racket 语言当中十分有趣的<code>big-bang</code>表达式，但在尝试设计一个世界的时候，其实会发现状态本身可能并不简单。交通灯只是一个枚举、座位号只是一个区间，那么像一个包含经验值、金币数等多方面的玩家数据呢？它该是什么。</p><p>本章会学习如何设计复合数据定义，以表示由两个或多个自然连接的值组成的信息，以及如何将这些数据用作 HtDW 问题中的世界状态。</p><h2 id="学习目标"><a class="markdownIt-Anchor" href="#学习目标"></a> 学习目标</h2><ul><li>能够识别应表示为复合数据的领域信息</li><li>能够读写<code>define-struct</code>定义</li><li>能够设计接受和/或返回复合数据的函数</li><li>能够设计使用复合世界状态的世界程序</li></ul><mark class="tag-plugin colorful mark" color="warning">以下内容涉及到的edX链接均不保证可访问性</mark><h2 id="define-struct"><a class="markdownIt-Anchor" href="#define-struct"></a> define-struct</h2><p>在之前，我们总是操作一些原子类型或者是稍显复杂的枚举、区间等，但对于复杂情况，比如表示<code>x</code>和<code>y</code>坐标，一个人的姓和名，我们该怎么做？</p><p>Racket 有一个能创建复合类型的表达式，即<code>define-struct</code>。以二维坐标为例，我们可以写出：<code>(define-struct pos (x y))</code>。这里<code>pos</code>是结构名，而后面的<code>(x y)</code>一类都是字段名。</p><p>尝试在一个空白的程序中运行这段代码，你会发现没有任何输出，这是因为<code>define-struct</code>和<code>define</code>一样，都是定义用的代码，不会实际产出什么的东西。也可以将<code>pos</code>理解为我们创造的复合类型，里面包含一个<code>x</code>值和一个<code>y</code>值。</p><hr /><p>我们总需要以某种方式<strong>得到</strong>一个<code>pos</code>类型的东西，可以为它写一个<emp>构造器</emp> <em>(Constructor)</em>。</p><p><strong>约定俗成的是</strong>，当你写出了<code>pos</code>这种复合类型，你同时得到了<code>make-pos</code>表达式，你可以往里面传两个值（对应<code>x</code>和<code>y</code>），来获得一个<code>pos</code>类型的值：<code>(define P1 (make-pos 3 6))</code>。</p><p>这时候<code>P1</code>就是一个<code>x=3</code>且<code>y=6</code>的<code>pos</code>类型值了。那么我们又该如何获取它的<code>x</code>和<code>y</code>呢？</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">pos-x</span> P1)<br>(<span class="hljs-name">pos-y</span> P1)<br></code></pre></td></tr></table></figure><p>运行后会发现交互区出现了<code>3</code>和<code>6</code>，也就是说不仅仅是<code>make-pos</code>，<code>pos-x</code>和<code>pos-y</code>也出现了。</p><p>接着就是和之前各种带<code>?</code>的表达式一样，我们也可以通过<code>pos?</code>表达式来判断后面的值是不是<code>pos</code>类型，比如：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">pos?</span> P1)       <span class="hljs-comment">; true</span><br>(<span class="hljs-name">pos?</span> <span class="hljs-string">&quot;hello&quot;</span>)  <span class="hljs-comment">; false</span><br></code></pre></td></tr></table></figure><details class="tag-plugin colorful folding" color="gray" open><summary><p>相关术语</p></summary><div class="body"><ul> <li>结构名 <em>(Structure Name)</em>：当你定义一个复合类型时，类型的名字</li> <li>字段名 <em>(Field Name)</em>：当你定义一个复合类型时，类型里面包含了哪些值</li> <li>构造器 <em>(Constructor)</em>：用于获取一个指定的复合类型，同时需要填满字段，通过<code>make-结构名</code>表达式。</li> <li>选择器 <em>(Selector)</em>：用于从给定复合类型获取里面的一个字段，通过<code>结构名-字段名</code>表达式。</li> <li>谓词 <em>(Predicate)</em>：用于判断一个表达式/值是否符合给定的复合类型，通过<code>结构名?</code>表达式。</li> </ul> <p><em>ps: 谓词这个翻译在这不太好</em></p> <p>比如<code>(define-struct pos (x y))</code>：</p> <ul> <li><code>make-pos</code> 是它的构造器表达式，接受两个字段值。</li> <li><code>pos-x</code>和<code>pos-y</code> 分别获取一个<code>pos</code>类型值里面的<code>x</code>和<code>y</code>值，接受一个<code>pos</code>类型值。</li> <li><code>pos?</code> 判断后面传入的一个值是否是<code>pos</code>类型的，返回布尔值。</li> </ul> </div></details><h2 id="compound-data-definitions"><a class="markdownIt-Anchor" href="#compound-data-definitions"></a> Compound Data Definitions</h2><p>本节会像之前第一次学到数据定义一样，细致地介绍如何定义一个复合类型。在此之前，<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/compound-starter.rkt">下载来自edX的 compound-starter.rkt 文件</a>。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1596/456;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-compound-data/compound-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-compound-data/compound-starter.webp" alt="compound-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">compound-starter.rkt</span></div></div><p>这道题要求我们设计一个数据结构，用于记录曲棍球员的姓和名，也就是说这两个值应该被绑在一块，<strong>形成一个复合类型</strong>。</p><p>首先，我们来尝试走一遍流程，先是类型定义本身：<code>(define-struct player (fn ln))</code>。</p><p>和普通数据定义一样，我们也要写注释声明<code>Player</code>：<code>;; Player is (make-player String String)</code>，这样我们既注明了它的构造器，也明确了<code>fn</code>和<code>ln</code>的类型是<code>String</code>。</p><p>那么我们该如何解释它呢？我们应该从<code>player</code>、<code>fn</code>和<code>ln</code>这三个名字开始，因为对于其他人来说他们可能并不知道这三个名字是什么意思：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; interp. (make-player fn ln) is a hockey player with</span><br><span class="hljs-comment">;;          fn is the first name</span><br><span class="hljs-comment">;;          ln is the last name</span><br></code></pre></td></tr></table></figure><p>然后就是一些例子，可以随便起一些名字：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> P1 (<span class="hljs-name">make-player</span> <span class="hljs-string">&quot;Bobby&quot;</span> <span class="hljs-string">&quot;Orr&quot;</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> P2 (<span class="hljs-name">make-player</span> <span class="hljs-string">&quot;Wayne&quot;</span> <span class="hljs-string">&quot;Gretzky&quot;</span>))<br></code></pre></td></tr></table></figure><p>接下来就是函数模板，既然它是个复合类型，那么获取它的两个字段值也是它模板函数需要用到的部分：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-player</span> p)<br>    (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">player-fn</span> p)     <span class="hljs-comment">; String</span><br>         (<span class="hljs-name">player-ln</span> p)))   <span class="hljs-comment">; String</span><br></code></pre></td></tr></table></figure><p>但它的模板规则就是带两个字段的复合类型：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;   - Compound: 2 fields</span><br></code></pre></td></tr></table></figure><p>完整定义如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">define-struct</span> player (<span class="hljs-name">fn</span> ln))<br><span class="hljs-comment">;; Player is (make-player String String)</span><br><span class="hljs-comment">;; interp. (make-player fn ln) is a hockey player with</span><br><span class="hljs-comment">;;          fn is the first name</span><br><span class="hljs-comment">;;          ln is the last name</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> P1 (<span class="hljs-name">make-player</span> <span class="hljs-string">&quot;Bobby&quot;</span> <span class="hljs-string">&quot;Orr&quot;</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> P2 (<span class="hljs-name">make-player</span> <span class="hljs-string">&quot;Wayne&quot;</span> <span class="hljs-string">&quot;Gretzky&quot;</span>))<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-player</span> p)<br>    (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">player-fn</span> p)     <span class="hljs-comment">; String</span><br>         (<span class="hljs-name">player-ln</span> p)))   <span class="hljs-comment">; String</span><br>         <br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;   - Compound: 2 fields</span><br></code></pre></td></tr></table></figure><h3 id="practice-problems"><a class="markdownIt-Anchor" href="#practice-problems"></a> Practice Problems</h3><ul><li>Compound P1 - Movie<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/movie-starter.rkt">movie-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/movie-solution.rkt">movie-solution.rkt</a></li></ul></li><li>Compound P3 - Student<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/student-starter.rkt">student-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/student-solution.rkt">student-solution.rkt</a></li></ul></li></ul><div class="tag-plugin colorful folders" ><details class="folder" index="0"><summary><p>Compound P1 - Movie 题解</p></summary><div class="body"><p><strong>预计耗时：25 min / 中等</strong></p><p>这道题有两个部分，分别是数据定义和函数设计。让我们先来看数据定义部分：你需要定义一个<code>movie</code>类型来记录一部电影的名字、预算和发布时间。</p><p>既然是复合类型，我们就需要使用<code>define-struct</code>表达式，后面跟三个不同类型的名字<code>(title budget year)</code>，同时标注这些类型是一个字符串和两个自然数，并给出解释：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">define-struct</span> movie (<span class="hljs-name">title</span> budget year))<br><span class="hljs-comment">;; Movie is (make-movie String Natural Natural)</span><br><span class="hljs-comment">;; interp. a movie with title, budget in USD, and year released</span><br></code></pre></td></tr></table></figure><p>借助构造器<code>make-movie</code>写三个例子：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> M1 (<span class="hljs-name">make-movie</span> <span class="hljs-string">&quot;Titanic&quot;</span> <span class="hljs-number">200000000</span> <span class="hljs-number">1997</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> M2 (<span class="hljs-name">make-movie</span> <span class="hljs-string">&quot;Avatar&quot;</span> <span class="hljs-number">237000000</span> <span class="hljs-number">2009</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> M3 (<span class="hljs-name">make-movie</span> <span class="hljs-string">&quot;The Avengers&quot;</span> <span class="hljs-number">220000000</span> <span class="hljs-number">2012</span>))<br></code></pre></td></tr></table></figure><p>因为它是带有三个字段的复合类型，我们写的模板函数也要照顾到这三个字段，通过<code>movie-字段名</code>来展开三个字段：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-movie</span> m)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">movie-title</span> m)    <span class="hljs-comment">;String</span><br>       (<span class="hljs-name">movie-budget</span> m)   <span class="hljs-comment">;Natural</span><br>       (<span class="hljs-name">movie-year</span> m)))   <span class="hljs-comment">;Natural</span><br></code></pre></td></tr></table></figure><p>以及比较好写的复合类型模板规则：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;; - compound: 3 fields</span><br></code></pre></td></tr></table></figure><p>函数定义部分代码：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">define-struct</span> movie (<span class="hljs-name">title</span> budget year))<br><span class="hljs-comment">;; Movie is (make-movie String Natural Natural)</span><br><span class="hljs-comment">;; interp. a movie with title, budget in USD, and year released</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> M1 (<span class="hljs-name">make-movie</span> <span class="hljs-string">&quot;Titanic&quot;</span> <span class="hljs-number">200000000</span> <span class="hljs-number">1997</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> M2 (<span class="hljs-name">make-movie</span> <span class="hljs-string">&quot;Avatar&quot;</span> <span class="hljs-number">237000000</span> <span class="hljs-number">2009</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> M3 (<span class="hljs-name">make-movie</span> <span class="hljs-string">&quot;The Avengers&quot;</span> <span class="hljs-number">220000000</span> <span class="hljs-number">2012</span>))<br><br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-movie</span> m)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">movie-title</span> m)    <span class="hljs-comment">;String</span><br>       (<span class="hljs-name">movie-budget</span> m)   <span class="hljs-comment">;Natural</span><br>       (<span class="hljs-name">movie-year</span> m)))   <span class="hljs-comment">;Natural</span><br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;; - compound: 3 fields</span><br></code></pre></td></tr></table></figure><hr /><p>数据定义部分结束，接下来是函数设计：判断传入的两个<code>Movie</code>的发布时间哪个更新，返回其名字。</p><p>从题目能理解出它的传入值类型是两个<code>Movie</code>，而返回值类型是<code>String</code>。同时函数的目的就是：<code>determine which of two given movies was released most recently</code>。</p><p>可以随意写一些测试，记得引用一开始的<code>M1</code>、<code>M2</code>和<code>M3</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">chronological-movie</span> M1 M2) <span class="hljs-string">&quot;Avatar&quot;</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">chronological-movie</span> M3 M2) <span class="hljs-string">&quot;The Avengers&quot;</span>)<br></code></pre></td></tr></table></figure><p>定义它的桩函数：<code>; (define (chronological-movie m1 m2) &quot;&quot;)   ; stub</code>。由于这里的实现与函数模板差别大，我们就不再写<code>use template from</code>之类的东西，而是现写新模板：函数名就是<code>fn-for-movie</code>，传入参数<code>m1</code>和<code>m2</code>，之后需要写上这俩所有的字段名：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-movie</span> m1 m2)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">movie-title</span> m1)<br>       (<span class="hljs-name">movie-budget</span> m1)<br>       (<span class="hljs-name">movie-year</span> m1)<br>       (<span class="hljs-name">movie-title</span> m2)<br>       (<span class="hljs-name">movie-budget</span> m2)<br>       (<span class="hljs-name">movie-year</span> m2)))<br></code></pre></td></tr></table></figure><p>最后实现函数，函数名和桩函数一样。对于函数体部分，我们只需要使用<code>if</code>表达式判断两个<code>movie-year</code>的大小，最后看情况返回对应<code>movie</code>的<code>title</code>就行：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">chronological-movie</span> m1 m2)<br>  (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> (<span class="hljs-name">movie-year</span> m1) (<span class="hljs-name">movie-year</span> m2))<br>      (<span class="hljs-name">movie-title</span> m1)<br>      (<span class="hljs-name">movie-title</span> m2)))<br></code></pre></td></tr></table></figure><p>函数设计代码如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Movie Movie -&gt; String</span><br><span class="hljs-comment">;; determine which of two given movies was released most recently</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">chronological-movie</span> M1 M2) <span class="hljs-string">&quot;Avatar&quot;</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">chronological-movie</span> M3 M2) <span class="hljs-string">&quot;The Avengers&quot;</span>)<br><br><span class="hljs-comment">; (define (chronological-movie m1 m2) &quot;&quot;)   ; stub</span><br><br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-movie</span> m1 m2)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">movie-title</span> m1)<br>       (<span class="hljs-name">movie-budget</span> m1)<br>       (<span class="hljs-name">movie-year</span> m1)<br>       (<span class="hljs-name">movie-title</span> m2)<br>       (<span class="hljs-name">movie-budget</span> m2)<br>       (<span class="hljs-name">movie-year</span> m2)))<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">chronological-movie</span> m1 m2)<br>  (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> (<span class="hljs-name">movie-year</span> m1) (<span class="hljs-name">movie-year</span> m2))<br>      (<span class="hljs-name">movie-title</span> m1)<br>      (<span class="hljs-name">movie-title</span> m2)))<br></code></pre></td></tr></table></figure></div></details><details class="folder" index="1"><summary><p>Compound P3 - Student 题解</p></summary><div class="body"><p><strong>预计耗时：25 min / 中等</strong></p><p>这道题也是相同的两个部分。让我们先来看数据定义部分：你需要定义一个<code>Student</code>，记录学生的名字、年级（从1到12）和是否存在过敏（为了准备出游的午餐）。</p><p><em>ps: 由于是否过敏这一项是布尔类型的，所以它会带问号</em></p><p>类型名就是<code>student</code>，带有三个字段名<code>(name grade allergies?)</code>，定义为<code>Student is (make-student String Natural[1, 12] Boolean)</code>，解释是<code>interp. a student with a name, in grade 1-12, and true if they have allergies</code>。之后写一些例子：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">define-struct</span> student (<span class="hljs-name">name</span> grade allergies?))<br><span class="hljs-comment">;; Student is (make-student String Natural[1, 12] Boolean)</span><br><span class="hljs-comment">;; interp. a student with a name, in grade 1-12, and true if they have allergies</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> S1 (<span class="hljs-name">make-student</span> <span class="hljs-string">&quot;Bob&quot;</span> <span class="hljs-number">2</span> true))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> S2 (<span class="hljs-name">make-student</span> <span class="hljs-string">&quot;Hannah&quot;</span> <span class="hljs-number">5</span> false))<br></code></pre></td></tr></table></figure><p>同样，它的函数模板也是用需要标明所有传入的字段，以及它的函数模板规则：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-student</span> s)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">student-name</span> s)         <span class="hljs-comment">;String</span><br>       (<span class="hljs-name">student-grade</span> s)        <span class="hljs-comment">;Natural[1, 12]</span><br>       (<span class="hljs-name">student-allergies?</span> s))) <span class="hljs-comment">;Boolean</span><br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;; - compound: 3 fields</span><br></code></pre></td></tr></table></figure><hr /><p>数据定义部分结束，接下来是函数设计：考虑到六年级及以下学生的过敏是很危险的，我们需要判断学生是否属于这种情况并添加到特殊列表中（这个列表只是用来起名字的，不需要实际实现）。</p><p>从题目能理解出它的传入值类型是<code>Student</code>，而返回值类型是<code>Boolean</code>。同时函数的目的就是：<code> produce true if the given student is at or below grade 6 and has allergies</code>。</p><p>考虑到函数存在两个判断，我们需要多写一些测试：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">add-name?</span> S1) true)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">add-name?</span> S2) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">add-name?</span> (<span class="hljs-name">make-student</span> <span class="hljs-string">&quot;Joanne&quot;</span> <span class="hljs-number">10</span> true)) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">add-name?</span> (<span class="hljs-name">make-student</span> <span class="hljs-string">&quot;John&quot;</span> <span class="hljs-number">11</span> false)) false)<br></code></pre></td></tr></table></figure><p>定义它的桩函数：<code>;(define (add-name? s) true)   ; stub</code>，并且声明将会用<code>Student</code>的函数模板：<code>;&lt;use template from Student&gt;</code></p><p>接下来将数据定义部分的函数模板复制下来，将函数名改为<code>add-name?</code>，并且根据测试来完善函数体。对于<strong>六年级及以下</strong>和<strong>是否有过敏</strong>，很显著的是我们需要使用<code>and</code>来进行与的判断：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">add-name?</span> s)<br>  (<span class="hljs-name"><span class="hljs-built_in">and</span></span> (<span class="hljs-name"><span class="hljs-built_in">&lt;=</span></span> (<span class="hljs-name">student-grade</span> s) <span class="hljs-number">6</span>)<br>       (<span class="hljs-name">student-allergies?</span> s)))<br></code></pre></td></tr></table></figure><p><em>ps: 如果用了<code>if</code>也是可以的</em></p><p>函数设计代码如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Student -&gt; Boolean</span><br><span class="hljs-comment">;; produce true if the given student is at or below grade 6 and has allergies</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">add-name?</span> S1) true)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">add-name?</span> S2) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">add-name?</span> (<span class="hljs-name">make-student</span> <span class="hljs-string">&quot;Joanne&quot;</span> <span class="hljs-number">10</span> true)) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">add-name?</span> (<span class="hljs-name">make-student</span> <span class="hljs-string">&quot;John&quot;</span> <span class="hljs-number">11</span> false)) false)<br><br><span class="hljs-comment">;(define (add-name? s) true)   ; stub</span><br><span class="hljs-comment">; Template from Student</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">add-name?</span> s)<br>  (<span class="hljs-name"><span class="hljs-built_in">and</span></span> (<span class="hljs-name"><span class="hljs-built_in">&lt;=</span></span> (<span class="hljs-name">student-grade</span> s) <span class="hljs-number">6</span>)<br>       (<span class="hljs-name">student-allergies?</span> s)))<br></code></pre></td></tr></table></figure></div></details></div><h2 id="htdw-with-compound-data"><a class="markdownIt-Anchor" href="#htdw-with-compound-data"></a> HtDW With Compound Data</h2><p>本节我们将基于复合类型设计一个世界。在之前，我们设计过带有一只猫的世界，现在我们将从一头牛开始。这头牛走出栅栏，过一会又转身回来。你会发现这个行为和猫不同 —— 猫只有一个轴上的坐标变化，而牛似乎还会涉及到方向。</p><p><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/cowabunga-starter.rkt">下载来自edX的 cowabunga-starter.rkt 文件</a>。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1884/1520;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-compound-data/cowabunga-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-compound-data/cowabunga-starter.webp" alt="cowabunga-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">cowabunga-starter.rkt</span></div></div><p>以下是图中两头牛的素材：</p><div class="tag-plugin grid"  style="grid-template-columns: repeat(2, 1fr);"><div class="cell" style="">    <div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:118/64;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-compound-data/cowabunga-starter-cow-left.webp" data-src="/images/2025/academics-ubc-cpsc-110-compound-data/cowabunga-starter-cow-left.webp" alt="左边的牛" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">左边的牛</span></div></div>    </div>    <div class="cell" style="">    <div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:122/64;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-compound-data/cowabunga-starter-cow-right.webp" data-src="/images/2025/academics-ubc-cpsc-110-compound-data/cowabunga-starter-cow-right.webp" alt="右边的牛" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">右边的牛</span></div></div>    </div>    </div><p>这道题很长，简单来说就是有头牛碰到屏幕边缘就来回走，用户也可以按下空格键来手动改变方向。按照上一章的步骤，我们先开始分析：</p><ul><li>常量：<ul><li>窗口的长宽是基本的，不会变</li><li>牛在往返走，故它的<code>y</code>坐标也不会变</li><li>牛本身的图像（左和右）和空白背景不会变</li></ul></li><li>变量：<ul><li>牛的<code>x</code>坐标在来回变化</li><li>牛每次碰壁需要转向，转向本身只会改变牛的图像，而它的速度被改变了（方向上）</li></ul></li><li><code>big-bang</code>选项：<ul><li><code>on-tick</code>和<code>on-draw</code>是最基本的</li><li><code>on-key</code>用来响应用户按下空格键的行为</li></ul></li></ul><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1353/829;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-compound-data/cowabunga-starter-analysis.webp" data-src="/images/2025/academics-ubc-cpsc-110-compound-data/cowabunga-starter-analysis.webp" alt="cowabunga-starter.rkt 分析" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">cowabunga-starter.rkt 分析</span></div></div><hr /><p>分析明白后，我们可以从<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/cowabunga-v0.rkt">edX的 cowabunga-v0.rkt 文件</a>开始，这个代码文件是本节项目的第一个，帮我们写了注释和常量定义。</p><p>那我们就从数据定义开始。由于牛的变化的量是它的<code>x</code>坐标和速度，它需要一个复合类型把它们装起来：<code>(define-struct cow (x dx))</code>，定义为：<code>Cow is (make-cow Natural[0, WIDTH], Integer)</code>。</p><p>它的解释需要更加详尽：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; interp. (make-cow x dx) is a cow with x coordinate x and x velocity dx</span><br><span class="hljs-comment">;;         the x is the center of the cow</span><br><span class="hljs-comment">;;         x  is in screen coordinates (pixels)</span><br><span class="hljs-comment">;;         dx is in pixels per tick</span><br></code></pre></td></tr></table></figure><p>之后就是它的例子，考虑到世界全程这头牛要么向左走要么向右走，我们就写这两种情况的例子：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> C1 (<span class="hljs-name">make-cow</span> <span class="hljs-number">10</span>  <span class="hljs-number">3</span>)) <span class="hljs-comment">; at 10, moving left -&gt; right</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> C2 (<span class="hljs-name">make-cow</span> <span class="hljs-number">20</span> <span class="hljs-number">-4</span>)) <span class="hljs-comment">; at 20, moving left &lt;- right</span><br></code></pre></td></tr></table></figure><p>以及它的函数模板和模板规则：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs scheme">#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-cow</span> c)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">cow-x</span> c)    <span class="hljs-comment">;Natural[0, WIDTH]</span><br>       (<span class="hljs-name">cow-dx</span> c))) <span class="hljs-comment">;Integer</span><br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;  - compound: 2 fields</span><br></code></pre></td></tr></table></figure><p>最后能得到<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/cowabunga-v1.rkt">edX的 cowabunga-v1.rkt 文件</a>，数据定义部分代码如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">define-struct</span> cow (<span class="hljs-name">x</span> dx))<br><span class="hljs-comment">;; Cow is (make-cow Natural[0, WIDTH] Integer)</span><br><span class="hljs-comment">;; interp. (make-cow x dx) is a cow with x coordinate x and x velocity dx</span><br><span class="hljs-comment">;;         the x is the center of the cow</span><br><span class="hljs-comment">;;         x  is in screen coordinates (pixels)</span><br><span class="hljs-comment">;;         dx is in pixels per tick</span><br><span class="hljs-comment">;;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> C1 (<span class="hljs-name">make-cow</span> <span class="hljs-number">10</span>  <span class="hljs-number">3</span>)) <span class="hljs-comment">; at 10, moving left -&gt; right</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> C2 (<span class="hljs-name">make-cow</span> <span class="hljs-number">20</span> <span class="hljs-number">-4</span>)) <span class="hljs-comment">; at 20, moving left &lt;- right</span><br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-cow</span> c)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">cow-x</span> c)    <span class="hljs-comment">;Natural[0, WIDTH]</span><br>       (<span class="hljs-name">cow-dx</span> c))) <span class="hljs-comment">;Integer</span><br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;  - compound: 2 fields</span><br></code></pre></td></tr></table></figure><hr /><p>为了效率，我们接下来直接从<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/cowabunga-v2.rkt">edX的 cowabunga-v2.rkt 文件</a>开始，这里帮我们写好了<code>big-bang</code>相关的表达式和桩函数。</p><p>先从<code>next-cow</code>函数开始，阅读它的目的能发现它需要让牛的<code>x</code>坐标增长，并根据是否在边缘来改变速度的方向。</p><p>它的测试需要考虑到三种情况：远离边缘时、到达边缘时、试图越过边缘时。</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">next-cow</span> (<span class="hljs-name">make-cow</span> <span class="hljs-number">20</span>           <span class="hljs-number">3</span>)) (<span class="hljs-name">make-cow</span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">20</span> <span class="hljs-number">3</span>)  <span class="hljs-number">3</span>)) <span class="hljs-comment">;away from edges</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">next-cow</span> (<span class="hljs-name">make-cow</span> <span class="hljs-number">20</span>          <span class="hljs-number">-3</span>)) (<span class="hljs-name">make-cow</span> (<span class="hljs-name"><span class="hljs-built_in">-</span></span> <span class="hljs-number">20</span> <span class="hljs-number">3</span>) <span class="hljs-number">-3</span>))<br><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">next-cow</span> (<span class="hljs-name">make-cow</span> (<span class="hljs-name"><span class="hljs-built_in">-</span></span> WIDTH <span class="hljs-number">3</span>)  <span class="hljs-number">3</span>)) (<span class="hljs-name">make-cow</span> WIDTH     <span class="hljs-number">3</span>)) <span class="hljs-comment">;reaches edge</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">next-cow</span> (<span class="hljs-name">make-cow</span> <span class="hljs-number">3</span>           <span class="hljs-number">-3</span>)) (<span class="hljs-name">make-cow</span> <span class="hljs-number">0</span>        <span class="hljs-number">-3</span>))<br><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">next-cow</span> (<span class="hljs-name">make-cow</span> (<span class="hljs-name"><span class="hljs-built_in">-</span></span> WIDTH <span class="hljs-number">2</span>)  <span class="hljs-number">3</span>)) (<span class="hljs-name">make-cow</span> WIDTH    <span class="hljs-number">-3</span>)) <span class="hljs-comment">;tries to pass edge</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">next-cow</span> (<span class="hljs-name">make-cow</span> <span class="hljs-number">2</span>           <span class="hljs-number">-3</span>)) (<span class="hljs-name">make-cow</span> <span class="hljs-number">0</span>         <span class="hljs-number">3</span>)) <br></code></pre></td></tr></table></figure><p>然后就是桩函数：<code>;(define (next-cow c) c)      ;stub</code>以及来自<code>cow</code>的函数模板，将它复制过来并改名。考虑到我们需要处理三种情况，可以使用<code>cond</code>表达式，在<code>Q</code>和<code>A</code>中都写上<code>cow</code>的两个字段：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">next-cow</span> c)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">cow-x</span> c)<br>              (<span class="hljs-name">cow-dx</span> c))<br>         (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">cow-x</span> c)<br>              (<span class="hljs-name">cow-dx</span> c))]<br>        [(<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">cow-x</span> c)<br>              (<span class="hljs-name">cow-dx</span> c))<br>         (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">cow-x</span> c)<br>              (<span class="hljs-name">cow-dx</span> c))]<br>        [(<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">cow-x</span> c)<br>              (<span class="hljs-name">cow-dx</span> c))<br>         (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">cow-x</span> c)<br>              (<span class="hljs-name">cow-dx</span> c))]))<br></code></pre></td></tr></table></figure><p><em>ps: 括号恐惧症犯了</em></p><p>这里就是<code>cond</code>的三种情况，分别处理：</p><ul><li>碰到屏幕右边</li><li>碰到屏幕左边</li><li>正常移动</li></ul><p>第一种情况，我们需要判断下一刻牛会不会撞上右边缘，这个逻辑该怎么写呢？如果牛的当前坐标和牛当前的速度相加发现能达到<code>WIDTH</code>，是不是意味着牛下一秒会撞到右边缘。之后的处理方法就是将牛安全的放在边缘，并让当前的速度方向取反：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">[(<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name">cow-x</span> c) (<span class="hljs-name">cow-dx</span> c)) WIDTH) <br>    (<span class="hljs-name">make-cow</span> WIDTH (<span class="hljs-name"><span class="hljs-built_in">-</span></span> (<span class="hljs-name">cow-dx</span> c)))]<br></code></pre></td></tr></table></figure><p>第二种情况和第一种就很像了，就是牛向左走的碰到左边缘的时候。这时候由于速度已经是负数了，那么我们直接将牛的<code>x</code>坐标和速度相加，判断它是否达到<code>0</code>以下就行了，同时将牛放到<code>0</code>的位置并以方向相反的速度移动：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">[(<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name">cow-x</span> c) (<span class="hljs-name">cow-dx</span> c)) <span class="hljs-number">0</span>)     <br>    (<span class="hljs-name">make-cow</span> <span class="hljs-number">0</span>     (<span class="hljs-name"><span class="hljs-built_in">-</span></span> (<span class="hljs-name">cow-dx</span> c)))]<br></code></pre></td></tr></table></figure><p>第三种情况就很简单了，就是牛在屏幕中晃悠：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">[<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>    (<span class="hljs-name">make-cow</span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name">cow-x</span> c) (<span class="hljs-name">cow-dx</span> c))<br>              (<span class="hljs-name">cow-dx</span> c))]<br></code></pre></td></tr></table></figure><p>最后的函数体代码如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">next-cow</span> c)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name">cow-x</span> c) (<span class="hljs-name">cow-dx</span> c)) WIDTH) (<span class="hljs-name">make-cow</span> WIDTH (<span class="hljs-name"><span class="hljs-built_in">-</span></span> (<span class="hljs-name">cow-dx</span> c)))]<br>        [(<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name">cow-x</span> c) (<span class="hljs-name">cow-dx</span> c)) <span class="hljs-number">0</span>)     (<span class="hljs-name">make-cow</span> <span class="hljs-number">0</span>     (<span class="hljs-name"><span class="hljs-built_in">-</span></span> (<span class="hljs-name">cow-dx</span> c)))]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span><br>         (<span class="hljs-name">make-cow</span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name">cow-x</span> c) (<span class="hljs-name">cow-dx</span> c))<br>                   (<span class="hljs-name">cow-dx</span> c))]))<br></code></pre></td></tr></table></figure><hr /><p>让我们从<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/cowabunga-v3.rkt">edX的 cowabunga-v3.rkt 文件</a>开始，这里已经做好了<code>next-cow</code>。这次我们开始写剩下的部分。</p><p>先是<code>render-cow</code>，先写它的测试：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-cow</span> (<span class="hljs-name">make-cow</span> <span class="hljs-number">99</span> <span class="hljs-number">3</span>))<br>              (<span class="hljs-name">place-image</span> RCOW <span class="hljs-number">99</span> CTR-Y MTS))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-cow</span> (<span class="hljs-name">make-cow</span> <span class="hljs-number">33</span> <span class="hljs-number">-3</span>))<br>              (<span class="hljs-name">place-image</span> LCOW <span class="hljs-number">33</span> CTR-Y MTS))<br></code></pre></td></tr></table></figure><details class="tag-plugin colorful folding" color="blue" open><summary><p>复合数据测试</p></summary><div class="body"><p>在测试复合数据时，尽可能让每个字段多变一变，这样能测试到更多东西。</p> </div></details><p>以及桩函数：<code>;(define (render-cow c) MTS)  ;stub</code>。从<code>cow</code>的函数模板复制下来，改名：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">; took template from Cow</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render-cow</span> c)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">cow-x</span> c)<br>       (<span class="hljs-name">cow-dx</span> c))) <br></code></pre></td></tr></table></figure><p>抛开显示哪头牛不谈，我们先用<code>place-image</code>显示牛本身：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render-cow</span> c)<br>  (<span class="hljs-name">place-image</span> ... (<span class="hljs-name">cow-x</span> c) CTR-Y MTS)) <br></code></pre></td></tr></table></figure><p>接下来就是<strong>封装</strong>的魅力了。对于显示哪头牛我们当然可以通过直接在<code>place-image</code>后面写个<code>if</code>判断方向正不正来返回对应的牛。但我们要遵循<strong>单一职责原则</strong> —— 在这里可以解释为一个函数不要干超过它预设职责的事情。</p><p>对于选择牛图像这件事，我们可以把它放在另一个函数里面，称作<code>choose-image</code>，传入一个<code>cow</code>。</p><p>先写上函数的签名<code>Cat -&gt; Image</code>，目的<code>produce RCOW or LCOW depending on direction cow is going</code>，以及特有的<code>!!!</code>。</p><p>它的测试需要包括向左走和向右走牛的图像选择：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">choose-image</span> (<span class="hljs-name">make-cow</span> <span class="hljs-number">10</span> <span class="hljs-number">3</span>))  RCOW)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">choose-image</span> (<span class="hljs-name">make-cow</span> <span class="hljs-number">11</span> <span class="hljs-number">-3</span>)) LCOW)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">choose-image</span> (<span class="hljs-name">make-cow</span> <span class="hljs-number">11</span> <span class="hljs-number">0</span>))  LCOW)  <span class="hljs-comment">; 默认情况</span><br></code></pre></td></tr></table></figure><p>之后从<code>cow</code>的模板函数复制过来，然后使用<code>if</code>表达式判断传入<code>cow</code>的<code>dx</code>是否为正，如果是那么就返回<code>RCOW</code>，否则就是<code>LCOW</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">choose-image</span> c)<br>  (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> (<span class="hljs-name">cow-dx</span> c) <span class="hljs-number">0</span>)<br>      RCOW<br>      LCOW))<br></code></pre></td></tr></table></figure><p>最后的键盘响应就不细说了：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Cow KeyEvent-&gt; Cow</span><br><span class="hljs-comment">;; reverse direction of cow travel when space bar is pressed</span><br><span class="hljs-comment">;; !!!</span><br><span class="hljs-comment">;(define (handle-key c ke) c) ;stub</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">handle-key</span> c ke)<br>  (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> ke <span class="hljs-string">&quot; &quot;</span>)<br>      (<span class="hljs-name">make-cow</span> (<span class="hljs-name">cow-x</span> c) (<span class="hljs-name"><span class="hljs-built_in">-</span></span> (<span class="hljs-name">cow-dx</span> c)))<br>      c))<br></code></pre></td></tr></table></figure><p>回过头来，我们做了<code>choose-image</code>的封装，那么<code>(place-image ... (cow-x c) CTR-Y MTS)</code>这个地方的<code>...</code>就可以变成<code>(choose-image c)</code>了。</p><p>如果不出意外，运行测试会成功，然后在交互区向<code>main</code>函数随便传个<code>cow</code>就行，比如：<code>(main (make-cow 10 5))</code>，回车后就能看到效果了。</p><h3 id="practice-problems-2"><a class="markdownIt-Anchor" href="#practice-problems-2"></a> Practice Problems</h3><ul><li>Compound P9 - Water Balloons<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/water-balloon-starter.rkt">water-balloon-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/water-balloon-solution.rkt">water-balloon-solution.rkt</a></li></ul></li></ul><div class="tag-plugin colorful folders" ><details class="folder" index="0"><summary><p>Compound P9 - Water Balloons 题解</p></summary><div class="body"><p><strong>预计耗时：90 min / 困难</strong></p><p>这道题看似复杂，实际上就是一个图像从左移动到右，同时在旋转。</p><p>那么经过分析，我们可以发现以下常量：</p><ul><li>有关窗口的宽高、空白场景</li><li>有关气球本身的图像，线速度和角速度，以及它不变的<code>y</code>坐标</li></ul><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> WIDTH  <span class="hljs-number">600</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> HEIGHT <span class="hljs-number">300</span>)<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> CTR-Y (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>))<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LINEAR-SPEED <span class="hljs-number">2</span>)  <br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> ANGULAR-SPEED <span class="hljs-number">3</span>) <span class="hljs-comment">;optional</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> MTS (<span class="hljs-name">rectangle</span> WIDTH HEIGHT <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;white&quot;</span>))<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> WATER-BALLOON &lt;气球图像&gt;)<br></code></pre></td></tr></table></figure><p>然后是数据定义，我们将这个世界的状态定为<code>BalloonState</code>，含有<code>x</code>和<code>a</code>两个字段，分别意为<code>x</code>坐标和角度：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">define-struct</span> bs (<span class="hljs-name">x</span> a))<br><span class="hljs-comment">;; BalloonState is (make-bs Number Number)</span><br><span class="hljs-comment">;; interp. The state of a tossed balloon.</span><br><span class="hljs-comment">;;         x is the x-coordinate in pixels</span><br><span class="hljs-comment">;;         a is the angle of rotation in degrees</span><br><span class="hljs-comment">;;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> BS1 (<span class="hljs-name">make-bs</span> <span class="hljs-number">10</span> <span class="hljs-number">0</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> BS2 (<span class="hljs-name">make-bs</span> <span class="hljs-number">30</span> <span class="hljs-number">15</span>))<br></code></pre></td></tr></table></figure><p>它的函数模板和规则如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs scheme">#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-balloon-state</span> bs)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> (<span class="hljs-name">bs-x</span> bs)<br>       (<span class="hljs-name">bs-a</span> bs)))<br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;; - compound: 2 fields</span><br></code></pre></td></tr></table></figure><hr /><p>然后就是函数设计部分，这一部分首先需要考虑的就是整体的<code>main</code>函数和里面的<code>big-bang</code>表达式。我们将<code>on-tick</code>的函数命名为<code>next-bs</code>、<code>to-draw</code>的函数命名为<code>render-bs</code>、以及<code>on-key</code>的函数命名为<code>reset-bs</code>。</p><p><code>main</code>函数本身就是<code>BalloonState -&gt; BalloonState</code>的持续运行的函数，用于实现动画。它的起始状态应为<code>(main (make-bs 0 0))</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; BalloonState -&gt; BalloonState</span><br><span class="hljs-comment">;; run the animation, starting with initial balloon state bs.</span><br><span class="hljs-comment">;; Start with (main (make-bs 0 0))</span><br><span class="hljs-comment">;; &lt;no tests for main functions&gt;</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">main</span> bs)<br>  (<span class="hljs-name">big-bang</span> bs<br>            (<span class="hljs-name">on-tick</span> next-bs)<br>            (<span class="hljs-name">to-draw</span> render-bs)<br>            (<span class="hljs-name">on-key</span>  reset-bs)))<br></code></pre></td></tr></table></figure><p>之后就是每个函数的处理了。对于<code>next-bs</code>函数，我们知道它的签名也是<code>BalloonState -&gt; BalloonState</code>，目的是调整下一次气球的线速度和角速度值。只需要让当前<code>bs-x</code>与线速度相加，<code>bs-a</code>与角速度相减（加减与旋转方向有关）就能处理：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; BalloonState -&gt; BalloonState</span><br><span class="hljs-comment">;; advance bs by LINEAR-SPEED and ANGULAR-SPEED</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">next-bs</span> (<span class="hljs-name">make-bs</span> <span class="hljs-number">1</span> <span class="hljs-number">12</span>))<br>              (<span class="hljs-name">make-bs</span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">1</span> LINEAR-SPEED) (<span class="hljs-name"><span class="hljs-built_in">-</span></span> <span class="hljs-number">12</span> ANGULAR-SPEED)))<br><br><span class="hljs-comment">;(define (next-bs bs) bs)  ;stub</span><br><span class="hljs-comment">; Template from BalloonState</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">next-bs</span> bs)<br>  (<span class="hljs-name">make-bs</span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name">bs-x</span> bs) LINEAR-SPEED)<br>           (<span class="hljs-name"><span class="hljs-built_in">-</span></span> (<span class="hljs-name">bs-a</span> bs) ANGULAR-SPEED)))<br></code></pre></td></tr></table></figure><p>这次的渲染函数<code>render-bs</code>需要注意，我们会使用<code>rotate</code>表达式来渲染旋转过的图像，但总体上还是<code>place-image</code>的。</p><p>但我们刚刚的实现 —— <code>(- 12 ANGULAR-SPEED)</code> —— 没有考虑小于<code>0</code>或者大于<code>360</code>的情况，也就是说角度会超出可用范围（这里是这样的）。我们需要使用某种方法来让这个值始终在<code>360</code>以内。</p><p>这时候就需要使用<code>modulo</code>表达式来<strong>取余</strong>。会发现比如<code>361</code>，如果使其对<code>360</code>取余，那么值就是<code>1</code>。如果是<code>720</code>，那就是<code>0</code>。我们可以通过这种方法来让值始终可用：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; BalloonState -&gt; Image</span><br><span class="hljs-comment">;; Produces the bs at height bs-x rotated (remainder bs-a 360) on the MTS</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-bs</span> (<span class="hljs-name">make-bs</span> <span class="hljs-number">1</span> <span class="hljs-number">12</span>))<br>              (<span class="hljs-name">place-image</span> (<span class="hljs-name">rotate</span> <span class="hljs-number">12</span> WATER-BALLOON)<br>                           <span class="hljs-number">1</span><br>                           CTR-Y<br>                           MTS))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-bs</span> (<span class="hljs-name">make-bs</span> <span class="hljs-number">10</span> <span class="hljs-number">361</span>))<br>              (<span class="hljs-name">place-image</span> (<span class="hljs-name">rotate</span> <span class="hljs-number">1</span> WATER-BALLOON)<br>                           <span class="hljs-number">10</span><br>                           CTR-Y<br>                           MTS))<br><br><span class="hljs-comment">; (define (render-bs bs) MTS)</span><br><br><span class="hljs-comment">; Template from BalloonState</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render-bs</span> bs)<br>  (<span class="hljs-name">place-image</span> (<span class="hljs-name">rotate</span> (<span class="hljs-name"><span class="hljs-built_in">modulo</span></span> (<span class="hljs-name">bs-a</span> bs) <span class="hljs-number">360</span>) WATER-BALLOON)<br>               (<span class="hljs-name">bs-x</span> bs)<br>               CTR-Y<br>               MTS))<br></code></pre></td></tr></table></figure><p>按空格键重置状态就很好写了，和之前都差不多：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; BalloonState KeyEvent -&gt; BalloonState</span><br><span class="hljs-comment">;; Resets the program so the balloon is back at the top, unrotated, when space bar is pressed</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">reset-bs</span> (<span class="hljs-name">make-bs</span> <span class="hljs-number">1</span> <span class="hljs-number">12</span>) <span class="hljs-string">&quot; &quot;</span>)<br>              (<span class="hljs-name">make-bs</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span>))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">reset-bs</span> (<span class="hljs-name">make-bs</span> <span class="hljs-number">1</span> <span class="hljs-number">12</span>) <span class="hljs-string">&quot;left&quot;</span>)<br>              (<span class="hljs-name">make-bs</span> <span class="hljs-number">1</span> <span class="hljs-number">12</span>))<br><br><span class="hljs-comment">; (define (reset-bs bs key) bs)</span><br><br><span class="hljs-comment">; Template from KeyEvent</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">reset-bs</span> bs key)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">key=?</span> <span class="hljs-string">&quot; &quot;</span> key) (<span class="hljs-name">make-bs</span> <span class="hljs-number">0</span> <span class="hljs-number">0</span>)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> bs]))<br></code></pre></td></tr></table></figure></div></details></div>]]>
    </content>
    <id>https://ziling.moe/2025/academics-ubc-cpsc-110-compound-data/</id>
    <link href="https://ziling.moe/2025/academics-ubc-cpsc-110-compound-data/"/>
    <published>2025-07-26T03:00:00.000Z</published>
    <summary>UBC 的计科大一必修课 - CPSC 110</summary>
    <title>UBC - CPSC 110 - Compound Data</title>
    <updated>2025-07-26T03:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term="Academics" scheme="https://ziling.moe/categories/Academics/"/>
    <category term="UBC" scheme="https://ziling.moe/tags/UBC/"/>
    <category term="计算机科学" scheme="https://ziling.moe/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    <category term="教程" scheme="https://ziling.moe/tags/%E6%95%99%E7%A8%8B/"/>
    <content>
      <![CDATA[<p>上一章我们着眼于数据结构设计，同时也让这些新数据类型和函数进行了联动。这一章会让程序更加融入现实，借由<code>big-bang</code>功能做出由键鼠控制的交互式程序。与此同时，我们将会带着之前所学的内容去系统化处理大型程序 —— 更复杂的数据类型与函数设计。</p><h2 id="学习目标"><a class="markdownIt-Anchor" href="#学习目标"></a> 学习目标</h2><ul><li>能够解释交互式图形程序的内在结构</li><li>能够运用《世界设计方法》(HtDW)来设计具有原子世界状态的交互程序</li><li>能够评估各元素在清晰度、简洁性及彼此一致性方面的表现</li><li>能够阅读和编写<code>big-bang</code>表达式</li></ul><mark class="tag-plugin colorful mark" color="warning">以下内容涉及到的edX链接均不保证可访问性</mark><h2 id="interactive-programs"><a class="markdownIt-Anchor" href="#interactive-programs"></a> Interactive Programs</h2><p>在之前，我们学习了：</p><ul><li>原子类型：如<code>String</code>、<code>Image</code>等</li><li>复杂类型：如<code>Enumeration</code>、<code>Interval</code>等</li><li>函数</li><li>条件表达式：<code>cond</code>、<code>if</code>等</li><li>…</li></ul><p>借此，结合一些实际场景，我们也将这些知识点融会贯通：通过座位号判断是否在走廊这一题就需要用到<code>Interval</code>，<code>cond</code>之类。</p><p>从这一章开始，我们将会从<em>更交互</em>的程序 —— 动画与游戏 —— 这种我们每日都在用的桌面应用程序开始。比如用鼠标在自己的窗口内点击，所在位置会出现一个升起的烟花，最后爆炸。</p><p>这些看似华丽的图形程序背后也是由许多基础的数据类型与函数组成的，有时也要结合一些数学知识。</p><h2 id="the-big-bang-mechanism"><a class="markdownIt-Anchor" href="#the-big-bang-mechanism"></a> The big-bang Mechanism</h2><p>在了解 <code>big-bang</code> 作为 Racket 的一种机制之前，我们先可以了解下它是如何运作的：设想存在一个窗口，里面只有一个数字，从10开始倒计时到0，每当人按下空格键就将倒计时清零。</p><p>这种交互式程序可以改变自身状态和显示内容，以及能够响应键鼠对其行为的影响。</p><p>在这个窗口背后，我们可以用一张表格来描述倒计时：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:776/822;width:300px;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-worlds/big-bang-countdown.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-worlds/big-bang-countdown.webp" alt="窗口行为表" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">窗口行为表</span></div></div><p>在这张表中，程序遵从<code>tick</code>来运行，而每个<code>tick</code>对应现实中的一秒。也就是说每过一秒，程序从<code>n=10</code>的状态变更为<code>n=9</code>，以此类推，同时这个数字对应的图像显示也会发生改变。</p><p>到了这里我们就能对程序的构建产生一些基础的想法了，比如说：</p><ul><li>定义<code>Countdown is Natural</code>来指定倒计时背后的数字类型</li><li><code>Countdown</code>的初始状态为<code>10</code></li><li>写一个函数，传入<code>Countdown</code>后得到它<code>-1</code>后的值</li><li>也是写一个函数来更新窗口的内容</li></ul><p>所以在这种程序内既需要函数用来<strong>运算逻辑</strong>，也需要用来<strong>渲染到窗口上</strong>的。</p><p>结合执行顺序来看，我们可以这么做：</p><ul><li>先在窗口上渲染初始值<code>10</code>，再获取它的下一个倒计时数字<code>9</code>，等待一秒</li><li>在窗口上渲染<code>9</code>，再获取它的下一个倒计时数字<code>8</code>，等待一秒</li><li>以此类推</li></ul><p>至此，<code>big-bang</code>表达式的基本构成也知道了：</p><ul><li><strong>初始状态</strong>：程序开始时的状态，类型是<code>Countdown</code></li><li><strong>状态更新函数</strong>：当每一个<code>tick</code>流逝，下一个更新后的状态，签名是<code>Countdown -&gt; Countdown</code></li><li><strong>渲染函数</strong>：当每一个<code>tick</code>流逝，将更新后的状态绘制到窗口上，签名是<code>Countdown -&gt; Image</code></li></ul><p>整个世界因为它们得以运行，这也是许多游戏引擎的根本运行逻辑，不然为什么叫<code>big-bang</code>这个名呢？</p><p>既然<code>big-bang</code>是强大的，那么它自然可以接受任何类型的状态用来处理和渲染。</p><h2 id="domain-analysis"><a class="markdownIt-Anchor" href="#domain-analysis"></a> Domain Analysis</h2><p>在上一节，我们了解了<code>big-bang</code>表达式的意义与用途。但其实在设计世界时，我们需要将事情分为两步，一步是需要动纸笔的分析，另一步才是真正的代码编写。</p><p><em>ps: 之后干脆都把设计这种交互式程序称作设计世界得了</em></p><p>在此之前，<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/cat-starter.rkt">下载来自edX的 cat-starter.rkt 文件</a>。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:892/841;width:500px;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-worlds/cat-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-worlds/cat-starter.webp" alt="cat-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">cat-starter.rkt</span></div></div><p>不要被冗长的题目吓到，它的大概意思是：设计一个世界，其中有一只猫从窗口的左边移动到右边并会一直移出窗口部分（也就是说如果太右边了猫就不见了）</p><p>参考 HtDW 的 Recipe，我们在纸笔分析的时候需要考虑：</p><ul><li>绘制程序的场景</li><li>识别不变和变化的信息</li><li>构思<code>big-bang</code>需要哪些函数</li></ul><details class="tag-plugin colorful folding" color="blue" open><summary><p>有关位置</p></summary><div class="body"><p>在形容一个元素在窗口中的位置时，是从窗口的左上角开始算的，如图：</p> <div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:594/444;width:300px;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-worlds/cat-starter-x-y-diagram.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-worlds/cat-starter-x-y-diagram.webp" alt="窗口位置示意图" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">窗口位置示意图</span></div></div> </div></details><p>让我们一个一个来，首先是绘制场景（当然用手画一画就行了，一只猫在一个矩形内从左移动到右），然后就是识别不变的信息。</p><p>在整个世界运行过程中，哪些信息不会变呢？这时候思维就要散开了：</p><ul><li>窗口本身的宽高不会变</li><li>猫本身这个图像的也不会变</li><li>猫是从左到右水平位移的，所以猫的<code>y</code>坐标也不会变，猫始终在竖直位置中心</li><li>还有一点比较难以发现，就是背景色始终是白色</li></ul><p>那么有什么会变呢？其实就只有猫的<code>x</code>坐标。</p><p>最后就是<code>big-bang</code>的内容了，参考 Recipe：</p><table><thead><tr><th>如果你的程序需要做到：</th><th>那么你就需要这个选项：</th></tr></thead><tbody><tr><td>随着时间流逝而更新状态</td><td>on-tick</td></tr><tr><td>显示</td><td>to-draw</td></tr><tr><td>响应键盘的键被按下</td><td>on-key</td></tr><tr><td>响应鼠标的活动</td><td>on-mouse</td></tr><tr><td>自动停止运行</td><td>stop-when</td></tr></tbody></table><p><code>on-tick</code>和<code>on-draw</code>是本次分析会用到的<code>big-bang</code>函数，所以也就这两个了。</p><p>最后程序的分析如图：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1353/829;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-worlds/cat-starter-domain-analysis.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-worlds/cat-starter-domain-analysis.webp" alt="Domain Analysis" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">Domain Analysis</span></div></div><p><em>ps: <code>ctr-y</code> 是指 <code>center-y</code>，即小猫在<code>y轴</code>上是在中心的</em></p><p><em>ps2: <code>MTS</code> 是指 <code>empty-scene</code>，即空场景</em></p><h2 id="program-through-main-function"><a class="markdownIt-Anchor" href="#program-through-main-function"></a> Program through main Function</h2><p>本节会延续上一节的代码部分开始。我们总是要为一个数据类型写一个完整的模板，从数据定义开始再到函数模板什么的。而<code>big-bang</code>设计的世界也需要一个模板，请将下方模板复制进 DrRacket：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">require</span></span> <span class="hljs-number">2</span>htdp/image)<br>(<span class="hljs-name"><span class="hljs-built_in">require</span></span> <span class="hljs-number">2</span>htdp/universe)<br><br><span class="hljs-comment">;; My world program  (make this more specific)</span><br><br><span class="hljs-comment">;; =================</span><br><span class="hljs-comment">;; Constants:</span><br><br><br><span class="hljs-comment">;; =================</span><br><span class="hljs-comment">;; Data definitions:</span><br><br><span class="hljs-comment">;; WS is ... (give WS a better name)</span><br><br><br><br><span class="hljs-comment">;; =================</span><br><span class="hljs-comment">;; Functions:</span><br><br><span class="hljs-comment">;; WS -&gt; WS</span><br><span class="hljs-comment">;; start the world with ...</span><br><span class="hljs-comment">;; </span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">main</span> ws)<br>  (<span class="hljs-name">big-bang</span> ws                   <span class="hljs-comment">; WS</span><br>            (<span class="hljs-name">on-tick</span>   tock)     <span class="hljs-comment">; WS -&gt; WS</span><br>            (<span class="hljs-name">to-draw</span>   render)   <span class="hljs-comment">; WS -&gt; Image</span><br>            (<span class="hljs-name">stop-when</span> ...)      <span class="hljs-comment">; WS -&gt; Boolean</span><br>            (<span class="hljs-name">on-mouse</span>  ...)      <span class="hljs-comment">; WS Integer Integer MouseEvent -&gt; WS</span><br>            (<span class="hljs-name">on-key</span>    ...)))    <span class="hljs-comment">; WS KeyEvent -&gt; WS</span><br><br><span class="hljs-comment">;; WS -&gt; WS</span><br><span class="hljs-comment">;; produce the next ...</span><br><span class="hljs-comment">;; !!!</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">tock</span> ws) ...)<br><br><br><span class="hljs-comment">;; WS -&gt; Image</span><br><span class="hljs-comment">;; render ... </span><br><span class="hljs-comment">;; !!!</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render</span> ws) ...)<br></code></pre></td></tr></table></figure><p>我们先按照模板填入我们的信息，程序部分的 Recipe 如下：</p><ul><li>定义一些常量</li><li>数据定义</li><li>函数（保证<code>big-bang</code>相关的函数在最后）</li></ul><p>对于首行的<code>My world program</code>，它应该作为我们程序的一句总结，可以细致地改为<code>A cat that walks from left to right across the screen.</code></p><div class="tag-plugin grid"  style="grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));"><div class="cell" style="">    <p>接下是常量部分，我们需要将上一节分析到的常量在<code>;; Constants:</code>后填进去：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> WIDTH <span class="hljs-number">600</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> HEIGHT <span class="hljs-number">400</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> CTR-Y (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> MTS (<span class="hljs-name">empty-scene</span> WIDTH HEIGHT))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> CAT-IMG &lt;一个小猫照片&gt;)<br></code></pre></td></tr></table></figure>    </div>    <div class="cell" style="">    <div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:75/117;height:100px;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-beginning-student-language-constant-cat.webp" data-src="/images/2025/academics-ubc-cpsc-110-beginning-student-language-constant-cat.webp" alt="小猫" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">小猫</span></div></div>    </div>    </div><p><code>CTR-Y</code>的值当然就是<code>HEIGHT</code>的一半，而<code>MTS</code>就是对应宽高的空白场景。</p><p>接下来就是数据定义部分，由于程序的核心在于让猫在<code>x</code>轴上动起来，所有我们可以定义<code>Cat is Number</code>替换掉<code>;; WS is ...</code>。</p><p>它的解释是：<code>interp. x position of the cat is screen coordinates</code>，之后为此写一些例子，对应小猫的位置：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> C1 <span class="hljs-number">0</span>)             <span class="hljs-comment">; left edge</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> C2 (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>))   <span class="hljs-comment">; middle</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> C3 WIDTH)         <span class="hljs-comment">; right edge</span><br></code></pre></td></tr></table></figure><p>因为<code>Number</code>只是个原子类型，<code>Cat</code>类型的函数模板就是：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-cat</span> c)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> c))<br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;   - atomic non-distinct: Number</span><br></code></pre></td></tr></table></figure><p>之后对于下面的主函数，我们需要将<code>WS</code>全部替换为<code>Cat</code>：</p><ul><li>在 Windows 上使用<kbd>Ctrl + F</kbd></li><li>在 MacOS 上使用<kbd>Command + F</kbd></li></ul><p>之后点击 DrRacket 下方的输入框的<code>Show Replace</code>，左侧填入<code>WS</code>，右侧填入<code>Cat</code>，按<code>Replace</code>即可替换。之后软件会匹配到下一个可替换项，如果这个项是<code>WS</code>的话就替换，如果是<code>ws</code>的话就按<code>Skip</code>跳过这个项。</p><p>对于带有<code>big-bang</code>表达式的<code>main</code>函数，我们需要将<code>ws</code>改为<code>c</code>，代表我们是围绕<code>Cat</code>类型操作的。</p><p>我们可以把主函数里面的<code>on-key</code>、<code>on-mouse</code>和<code>stop-when</code>删了，因为用不到这些情况。同时将<code>tock</code>改为<code>advance-cat</code>，代表每一刻实际做的事就是让猫移动。</p><hr /><p>从这里能看出来我们的主函数需要调用另外两个函数，分别是<code>advance-cat</code>和<code>render</code>。</p><p>对于下面的第一个函数，即之前还没改名的<code>tock</code>（现在记得改名），它的目的是<code>produce the next cat, by advancing it 1 pixel to right</code>。而下面<code>render</code>的目的是<code>render the cat image at appropriate place on MTS</code></p><p>这两个函数统称为<code>wish-list entry</code>，它包含一个签名、目的、<code>!!!</code>和桩函数。在设计世界的时候可以以此作为待办事项之类的东西，在之后回来实现它们：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Cat -&gt; Cat</span><br><span class="hljs-comment">;; produce the next cat, by advancing it 1 pixel to right</span><br><span class="hljs-comment">;; !!!</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">advance-cat</span> c) <span class="hljs-number">0</span>)<br><br><br><span class="hljs-comment">;; Cat -&gt; Image</span><br><span class="hljs-comment">;; render the cat image at appropriate place on MTS </span><br><span class="hljs-comment">;; !!!</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render</span> c) MTS)<br></code></pre></td></tr></table></figure><details class="tag-plugin colorful folding" color="blue" open><summary><p>有关!!!</p></summary><div class="body"><p>这是一种标记，在之后可以通过全局搜索<code>!!!</code>来找哪些函数没写。</p> </div></details><p>点击运行后，我们在下方的交互区输入<code>(main 0)</code>回车，能得到一个完全空白的窗口。</p><p>本节完整代码如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">require</span></span> <span class="hljs-number">2</span>htdp/image)<br>(<span class="hljs-name"><span class="hljs-built_in">require</span></span> <span class="hljs-number">2</span>htdp/universe)<br><br><span class="hljs-comment">;; A cat that walks from left to right across the screen.</span><br><br><span class="hljs-comment">;; =================</span><br><span class="hljs-comment">;; Constants:</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> WIDTH <span class="hljs-number">600</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> HEIGHT <span class="hljs-number">400</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> CTR-Y (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> MTS (<span class="hljs-name">empty-scene</span> WIDTH HEIGHT))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> CAT-IMG &lt;猫图&gt;)<br><br><span class="hljs-comment">;; =================</span><br><span class="hljs-comment">;; Data definitions:</span><br><br><span class="hljs-comment">;; Cat is Number</span><br><span class="hljs-comment">;; interp. x position of the cat is screen coordinates</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> C1 <span class="hljs-number">0</span>)             <span class="hljs-comment">; left edge</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> C2 (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>))   <span class="hljs-comment">; middle</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> C3 WIDTH)         <span class="hljs-comment">; right edge</span><br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-cat</span> c)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> c))<br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;   - atomic non-distinct: Number</span><br><span class="hljs-comment">;; =================</span><br><span class="hljs-comment">;; Functions:</span><br><br><span class="hljs-comment">;; Cat -&gt; Cat</span><br><span class="hljs-comment">;; start the world with ...</span><br><span class="hljs-comment">;; </span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">main</span> c)<br>  (<span class="hljs-name">big-bang</span> c                     <span class="hljs-comment">; Cat</span><br>            (<span class="hljs-name">on-tick</span>   advance-cat)      <span class="hljs-comment">; Cat -&gt; Cat</span><br>            (<span class="hljs-name">to-draw</span>   render)))  <span class="hljs-comment">; Cat -&gt; Image</span><br><br><span class="hljs-comment">;; Cat -&gt; Cat</span><br><span class="hljs-comment">;; produce the next cat, by advancing it 1 pixel to right</span><br><span class="hljs-comment">;; !!!</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">advance-cat</span> c) <span class="hljs-number">0</span>)<br><br><br><span class="hljs-comment">;; Cat -&gt; Image</span><br><span class="hljs-comment">;; render the cat image at appropriate place on MTS </span><br><span class="hljs-comment">;; !!!</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render</span> c) MTS)<br></code></pre></td></tr></table></figure><h2 id="working-through-the-wish-list"><a class="markdownIt-Anchor" href="#working-through-the-wish-list"></a> Working through the Wish List</h2><p>找到<code>advance-cat</code>的桩函数，我们现在就要完善它。</p><p>首先，<code>Cat</code>类型就是<code>Number</code>，代表<code>x</code>轴坐标，而这个函数需要将传入的<code>Cat</code>向右移动一个像素，所以我们的测试可以是<code>(check-expect (advance-cat 3) 4)</code>。</p><p>之后声明从<code>Cat</code>的数据定义拿到函数模板，就可以从<code>Cat</code>那里将函数模板复制到这里，将函数名改为<code>advance-cat</code>，函数体就是简单的<code>(+ c 1)</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Cat -&gt; Cat</span><br><span class="hljs-comment">;; produce the next cat, by advancing it 1 pixel to right</span><br><span class="hljs-comment">;; !!!</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">advance-cat</span> <span class="hljs-number">3</span>) <span class="hljs-number">4</span>)<br><span class="hljs-comment">;;(define (advance-cat c) 0)</span><br><span class="hljs-comment">;&lt;use template from Cat&gt;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">advance-cat</span> c)<br>  (<span class="hljs-name"><span class="hljs-built_in">+</span></span> c <span class="hljs-number">1</span>))<br></code></pre></td></tr></table></figure><p>运行后得到测试通过的结果就没什么问题了。</p><hr /><p>之后就是下面的<code>render</code>函数，一开始可能会很疑惑如何将图片放到场景里。在 Racket 里，可以使用<code>place-image</code>表达式，这个表达式需要四个东西：</p><ul><li>你要放进去的图片，这里我们就用之前定义的<code>CAT-IMG</code></li><li><code>x</code>轴坐标，是个数字</li><li><code>y</code>轴坐标，猫的<code>y</code>轴坐标不变，就是常量<code>CTR-Y</code></li><li>场景，即常量<code>MTS</code></li></ul><p>所以<code>render</code>函数接受可变量<code>x</code>轴坐标，然后将小猫放在对于位置上，测试可以这么写：<code>(check-expect (render 4) (place-image CAT-IMG 4 CTR-Y MTS))</code></p><p><em>ps: 这时候运行你会得到一个测试错误，并附带一个大白窗口</em></p><p>由于传入值是<code>Cat</code>（即<code>Number</code>，或是说<code>x</code>轴坐标），<code>render</code>函数同样可以从<code>Cat</code>复制来函数模板。</p><p>之后再将<code>place-image</code>表达式放到函数体里面，由于<code>c</code>就是<code>x</code>轴坐标，<code>place-image</code>里的<code>x</code>轴坐标部分就是<code>c</code>了：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Cat -&gt; Image</span><br><span class="hljs-comment">;; render the cat image at appropriate place on MTS </span><br><span class="hljs-comment">;; !!!</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render</span> <span class="hljs-number">4</span>) (<span class="hljs-name">place-image</span> CAT-IMG <span class="hljs-number">4</span> CTR-Y MTS))<br><span class="hljs-comment">;(define (render c) MTS)  ; stub</span><br><span class="hljs-comment">;&lt;use template from Cat&gt;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render</span> c)<br>  (<span class="hljs-name">place-image</span> CAT-IMG c CTR-Y MTS))<br></code></pre></td></tr></table></figure><p>运行后会先提示测试成功，之后在交互区输入<code>(main 0)</code>，弹出来的窗口就是猫在移动的动画。</p><p>本节<code>advance-cat</code>和<code>render</code>函数部分代码如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Cat -&gt; Cat</span><br><span class="hljs-comment">;; produce the next cat, by advancing it 1 pixel to right</span><br><span class="hljs-comment">;; !!!</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">advance-cat</span> <span class="hljs-number">3</span>) <span class="hljs-number">4</span>)<br><span class="hljs-comment">;;(define (advance-cat c) 0)</span><br><span class="hljs-comment">;&lt;use template from Cat&gt;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">advance-cat</span> c)<br>  (<span class="hljs-name"><span class="hljs-built_in">+</span></span> c <span class="hljs-number">1</span>))<br><br><br><span class="hljs-comment">;; Cat -&gt; Image</span><br><span class="hljs-comment">;; render the cat image at appropriate place on MTS </span><br><span class="hljs-comment">;; !!!</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render</span> <span class="hljs-number">4</span>) (<span class="hljs-name">place-image</span> CAT-IMG <span class="hljs-number">4</span> CTR-Y MTS))<br><span class="hljs-comment">;(define (render c) MTS)  ; stub</span><br><span class="hljs-comment">;&lt;use template from Cat&gt;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render</span> c)<br>  (<span class="hljs-name">place-image</span> CAT-IMG c CTR-Y MTS))<br></code></pre></td></tr></table></figure><h2 id="improving-a-world-program"><a class="markdownIt-Anchor" href="#improving-a-world-program"></a> Improving a World Program</h2><h3 id="add-speed"><a class="markdownIt-Anchor" href="#add-speed"></a> Add SPEED</h3><p>程序在很多时候不会一设计好就不再更改了，哪怕是一个很完备的程序，它的用户也希望它变得更好。</p><p>本节会基于我们在之前编写的程序从功能性上做出一些更新，<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/cat-v1.rkt">下载来自edX的 cat-v1.rkt 文件</a>。</p><p><em>ps: 这个程序已经完成了之前几节的内容</em></p><p>运行通过测试后，在交互区输入<code>(main 0)</code>就能弹出窗口了。小猫同时开始从窗口左边向右平移。</p><p>但它的速度实在是慢了点，我们或许可以加加速。如果考虑到速度，它也是一个常量，<em>因为我们没考虑变速</em>。</p><p>目前的速度是<code>1 pixel/tick</code>，试下它的三倍速，在常量定义那加一行<code>(define SPEED 3)</code>。</p><p>在之前，我们默认将速度设为<code>1</code>，让<code>advance-cat</code>函数的测试写成<code>(check-expect (advance-cat 3) 4)</code>，因为这个函数会得到猫的下一个所在位置，也就是<code>3+1=4</code>，但这时候这个<code>+1</code>变成了<code>+SPEED</code>。位置变化和速度相关，所以这里应该改成<code>(check-expect (advance-cat 3) (+ 3 SPEED))</code>。</p><p>后面的<code>advance-cat</code>函数体同理。</p><p>这个过程看着比较简单，但实际上在增减功能的时候我们总是要从全局考虑，比如这样设计会不会破坏测试，会不会破坏其他函数的兼容性。</p><h3 id="add-key-handler"><a class="markdownIt-Anchor" href="#add-key-handler"></a> Add key handler</h3><p>题目中描述到需要响应键盘案件，如果按下空格键，会让猫回到最左边重新移动一遍。</p><p>本节会处理这个需求，<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/cat-v2.rkt">下载来自edX的 cat-v2.rkt 文件</a>。</p><p><em>ps: 这个文件把在v1的基础上完成了上节的内容</em></p><p>从需求出发，我们需要响应空格键被按下这个事件。这里就需要用到在一开始了解<code>big-bang</code>表达式时候被删掉的一个选项了 —— <code>on-key</code>。</p><p>我们将这个选项加入到程序的<code>big-bang</code>内：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">main</span> c)<br>  (<span class="hljs-name">big-bang</span> c                       <span class="hljs-comment">; Cat</span><br>            (<span class="hljs-name">on-tick</span>   advance-cat) <span class="hljs-comment">; Cat -&gt; Cat</span><br>            (<span class="hljs-name">to-draw</span>   render)      <span class="hljs-comment">; Cat -&gt; Image</span><br>            (<span class="hljs-name">on-key</span>    ...)))       <span class="hljs-comment">; Cat KeyEvent -&gt; Cat</span><br></code></pre></td></tr></table></figure><p>之后就要具体设计这个函数了，到程序的末尾，写上函数的签名：<code>;; Cat Event -&gt; Cat</code>和目的：<code>;; reset cat to left edge when space key is pressed</code>。</p><p>这个函数负责处理键盘事件，即<code>Key Handler</code>，所以它会<code>handle key</code>，桩函数就是：<code>(define (handle-key c ke) 0)  ; stub</code>。</p><p>这时候就可以补齐上面的<code>big-bang</code>选项了：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">main</span> c)<br>  (<span class="hljs-name">big-bang</span> c                        <span class="hljs-comment">; Cat</span><br>            (<span class="hljs-name">on-tick</span>   advance-cat)  <span class="hljs-comment">; Cat -&gt; Cat</span><br>            (<span class="hljs-name">to-draw</span>   render)       <span class="hljs-comment">; Cat -&gt; Image</span><br>            (<span class="hljs-name">on-key</span>    handle-key))) <span class="hljs-comment">; Cat KeyEvent -&gt; Cat</span><br></code></pre></td></tr></table></figure><p>设计函数也是要测试的，那么<code>ke</code>该填什么呢？</p><details class="tag-plugin colorful folding" color="gray" open><summary><p>KeyEvent</p></summary><div class="body"><p><code>KeyEvent</code> 是一个巨大的枚举，包含字母表中的所有字母以及您可以在键盘上按下的其他键，比如响应<code>a</code>键，值就为<code>&quot;a&quot;</code>。详情请使用<code>Help Desk</code>查询。</p> </div></details><p>我们需要响应空格键，那么就应该是<code>&quot; &quot;</code>了，对于其他按键就不管了：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">handle-key</span> <span class="hljs-number">10</span> <span class="hljs-string">&quot; &quot;</span>) <span class="hljs-number">0</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">handle-key</span> <span class="hljs-number">10</span> <span class="hljs-string">&quot;a&quot;</span>) <span class="hljs-number">10</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">handle-key</span> <span class="hljs-number">0</span> <span class="hljs-string">&quot; &quot;</span>) <span class="hljs-number">0</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">handle-key</span> <span class="hljs-number">0</span> <span class="hljs-string">&quot;a&quot;</span>) <span class="hljs-number">0</span>)<br></code></pre></td></tr></table></figure><p>既然<code>KeyEvent</code>是个大枚举，我们岂不是得把所有的情况都考虑到？对于这种<emp>大枚举</emp> <em>(Large Enumeration)</em>，我们往往不会枚举所有的可能性，只需要多考虑下预期和超出预期的状态就行了。</p><p>Recipe 中的<code>on-key</code>模板如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">handle-key</span> ws ke)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">key=?</span> ke <span class="hljs-string">&quot; &quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">...</span></span> ws)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> <br>         (<span class="hljs-name"><span class="hljs-built_in">...</span></span> ws)]))<br></code></pre></td></tr></table></figure><p>我们将其按照桩函数改造，并开始实现它。刚好模板的<code>cond</code>表达式确实将空格键和其他按键算作两个情况处理，那我们只需要在<code>(key=? ke &quot; &quot;)</code>对应的<code>A</code>设为<code>0</code>就行，其他维持传入的<code>c</code>不变，得到：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">handle-key</span> c ke)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">key=?</span> ke <span class="hljs-string">&quot; &quot;</span>) <span class="hljs-number">0</span>]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> c]))<br></code></pre></td></tr></table></figure><p>测试通过后运行，在弹出的窗口按空格键就能看到效果了。</p><h2 id="practice-problems"><a class="markdownIt-Anchor" href="#practice-problems"></a> Practice Problems</h2><p>这一章的 Recommended Problems:</p><ul><li>HtDW P1 - Countdown<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/countdown-animation-starter.rkt">countdown-animation-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/countdown-animation-solution.rkt">countdown-animation-solution.rkt</a></li></ul></li><li>HtDW P2 - Traffic Light<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/traffic-light-starter.rkt">traffic-light-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/traffic-light-solution.rkt">traffic-light-solution.rkt</a></li></ul></li></ul><p><em>ps: 做完题才发现这模板太严了，考虑到篇幅问题，题解会速通很多地方</em></p><details class="tag-plugin colorful folding" color="gray" open><summary><p>模板</p></summary><div class="body"><p>以下两道题都可以基于以下模板来做：</p> <figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">require</span></span> <span class="hljs-number">2</span>htdp/image)<br>(<span class="hljs-name"><span class="hljs-built_in">require</span></span> <span class="hljs-number">2</span>htdp/universe)<br><br><span class="hljs-comment">;; My world program  (make this more specific)</span><br><br><span class="hljs-comment">;; =================</span><br><span class="hljs-comment">;; Constants:</span><br><br><br><span class="hljs-comment">;; =================</span><br><span class="hljs-comment">;; Data definitions:</span><br><br><span class="hljs-comment">;; WS is ... (give WS a better name)</span><br><br><br><br><span class="hljs-comment">;; =================</span><br><span class="hljs-comment">;; Functions:</span><br><br><span class="hljs-comment">;; WS -&gt; WS</span><br><span class="hljs-comment">;; start the world with ...</span><br><span class="hljs-comment">;; </span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">main</span> ws)<br>  (<span class="hljs-name">big-bang</span> ws                   <span class="hljs-comment">; WS</span><br>            (<span class="hljs-name">on-tick</span>   tock)     <span class="hljs-comment">; WS -&gt; WS</span><br>            (<span class="hljs-name">to-draw</span>   render)   <span class="hljs-comment">; WS -&gt; Image</span><br>            (<span class="hljs-name">stop-when</span> ...)      <span class="hljs-comment">; WS -&gt; Boolean</span><br>            (<span class="hljs-name">on-mouse</span>  ...)      <span class="hljs-comment">; WS Integer Integer MouseEvent -&gt; WS</span><br>            (<span class="hljs-name">on-key</span>    ...)))    <span class="hljs-comment">; WS KeyEvent -&gt; WS</span><br><br><span class="hljs-comment">;; WS -&gt; WS</span><br><span class="hljs-comment">;; produce the next ...</span><br><span class="hljs-comment">;; !!!</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">tock</span> ws) ...)<br><br><br><span class="hljs-comment">;; WS -&gt; Image</span><br><span class="hljs-comment">;; render ... </span><br><span class="hljs-comment">;; !!!</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render</span> ws) ...)<br></code></pre></td></tr></table></figure> </div></details><div class="tag-plugin colorful folders" ><details class="folder" index="0"><summary><p>HtDW P1 - Countdown 题解</p></summary><div class="body"><p><strong>预计耗时：30 min / 简单</strong></p><p>这道题让我们实现一个从10开始每秒倒数的倒计时程序，我们可以开始思考常量有哪些：</p><ul><li>窗口的长宽和空白背景</li><li>倒计时文本所处的位置、文字大小和颜色（这些是因为<code>text</code>表达式强制需要这些）</li></ul><p>至此我们就能定义以下常量：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> WIDTH <span class="hljs-number">50</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> HEIGHT <span class="hljs-number">50</span>)<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> CTR-X (<span class="hljs-name"><span class="hljs-built_in">/</span></span> WIDTH <span class="hljs-number">2</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> CTR-Y (<span class="hljs-name"><span class="hljs-built_in">/</span></span> HEIGHT <span class="hljs-number">2</span>))<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> MTS (<span class="hljs-name">empty-scene</span> WIDTH HEIGHT))<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> TEXT-SIZE <span class="hljs-number">24</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> TEXT-COLOR <span class="hljs-string">&quot;black&quot;</span>)<br></code></pre></td></tr></table></figure><p>到了数据定义部分：其实倒计时本身就是个从0到10的自然数区间，用于显示倒计时中剩余的秒数。由于是区间，我们需要覆盖三个测试，并给出阶段性的解释：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Countdown is Natural[0, 10]</span><br><span class="hljs-comment">;; interp. the current seconds remaining in the countdown</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> CD1 <span class="hljs-number">10</span>)  <span class="hljs-comment">;countdown hasn&#x27;t started</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> CD2 <span class="hljs-number">5</span>)   <span class="hljs-comment">;countdown in progress</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> CD3 <span class="hljs-number">0</span>)   <span class="hljs-comment">;countdown finished</span><br></code></pre></td></tr></table></figure><p>之后我们要写它的模板函数：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme">#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-countdown</span> cd)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> cd))<br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;; - atomic non-distinct: Natural[0, 10]</span><br></code></pre></td></tr></table></figure><hr /><p>接下来就是最复杂的函数部分，对于第一个<code>big-bang</code>表达式的主函数，我们只需要明确里面的名字和目的就行，同时题目要求<code>on-tick</code>函数应叫<code>advance-countdown</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Countdown -&gt; Countdown</span><br><span class="hljs-comment">;; called to run the animation; start with (main 10)</span><br><span class="hljs-comment">;; no tests for main function</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">main</span> cd)<br>  (<span class="hljs-name">big-bang</span> cd<br>            (<span class="hljs-name">on-tick</span> advance-countdown <span class="hljs-number">1</span>)   <span class="hljs-comment">; Countdown -&gt; Countdown</span><br>            (<span class="hljs-name">to-draw</span> render-countdown)      <span class="hljs-comment">; Countdown -&gt; Image</span><br>            (<span class="hljs-name">on-key</span> handle-key)))           <span class="hljs-comment">; Countdown KeyEvent -&gt; Countdown</span><br></code></pre></td></tr></table></figure><details class="tag-plugin colorful folding" color="blue" open><summary><p>on-tick</p></summary><div class="body"><p>这里的<code>on-tick</code>表达式后面能跟个<code>1</code>的意思是每经过1秒执行一次。</p> </div></details><p><code>advance-countdown</code>其实就是一个一直减一的函数，传进去的数都会减1，直到<code>0</code>。在写好测试和桩函数，并从模板函数复制过来后，考虑在函数体内使用<code>if</code>表达式将<code>0</code>与非<code>0</code>的情况分别处理就行：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Countdown -&gt; Countdown</span><br><span class="hljs-comment">;; if cd is zero, produce zero, otherwise subtract 1</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">advance-countdown</span> <span class="hljs-number">10</span>) <span class="hljs-number">9</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">advance-countdown</span> <span class="hljs-number">1</span>) <span class="hljs-number">0</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">advance-countdown</span> <span class="hljs-number">0</span>) <span class="hljs-number">0</span>)<br><br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">advance-countdown</span> cd) <span class="hljs-number">0</span>) <span class="hljs-comment">; stub</span><br><span class="hljs-comment">;&lt;template from Countdown&gt;</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">advance-countdown</span> cd)<br>  (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">=</span></span> <span class="hljs-number">0</span> cd)<br>      <span class="hljs-number">0</span><br>      (<span class="hljs-name"><span class="hljs-built_in">-</span></span> cd <span class="hljs-number">1</span>)))<br></code></pre></td></tr></table></figure><p>渲染函数的目的就很简单了，就是把一个数字放在窗口上。准确来说是将数字变成字符串类型后，通过<code>text</code>表达式使其变成<code>Image</code>，再通过<code>place-image</code>表达式将其放在窗口空白背景的某个位置上，写好测试、桩函数和模板函数复制后，我们就能将函数体实现了：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Countdown -&gt; Image</span><br><span class="hljs-comment">;; produce an appropriate image for the countdown</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-countdown</span> <span class="hljs-number">10</span>) (<span class="hljs-name">place-image</span> (<span class="hljs-name">text</span> <span class="hljs-string">&quot;10&quot;</span> TEXT-SIZE TEXT-COLOR) <br>                                                 CTR-X CTR-Y<br>                                                 MTS))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-countdown</span> <span class="hljs-number">0</span>) (<span class="hljs-name">place-image</span> (<span class="hljs-name">text</span> <span class="hljs-string">&quot;0&quot;</span> TEXT-SIZE TEXT-COLOR)<br>                                                CTR-X CTR-Y<br>                                                MTS))<br><br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render-countdown</span> cd) MTS) <span class="hljs-comment">;stub</span><br><span class="hljs-comment">;&lt;template from Countdown&gt;</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render-countdown</span> cd)<br>  (<span class="hljs-name">place-image</span> (<span class="hljs-name">text</span> (<span class="hljs-name"><span class="hljs-built_in">number-&gt;string</span></span> cd) TEXT-SIZE TEXT-COLOR)<br>               CTR-X CTR-Y<br>               MTS))<br></code></pre></td></tr></table></figure><details class="tag-plugin colorful folding" color="blue" open><summary><p>里面用到的表达式</p></summary><div class="body"><p>复习一下：</p> <ul> <li><code>number-&gt;string</code> 接受一个数字，返回一个它的字符串类型值</li> <li><code>text</code> 将传入的字符串以某种字号和颜色变成图片</li> <li><code>place-image</code> 将图片放在某个场景的一个位置上</li> </ul> </div></details><p>最后就是附加题，当用户按下空格键，倒计时就会回到<code>10</code>。对于<code>handle-key</code>这个函数来说，它只需要传入当前的倒计时和<code>KeyEvent</code>，返回一个新的倒计时数就行，我们可以在函数体内使用<code>cond</code>表达式之类的，如果遇到空格就返回<code>10</code>，否则不变：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Countdown KeyEvent -&gt; Countdown</span><br><span class="hljs-comment">;; reset the countdown to 10 when the spacebar is pressed</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">handle-key</span> <span class="hljs-number">0</span> <span class="hljs-string">&quot; &quot;</span>) <span class="hljs-number">10</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">handle-key</span> <span class="hljs-number">5</span> <span class="hljs-string">&quot; &quot;</span>) <span class="hljs-number">10</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">handle-key</span> <span class="hljs-number">5</span> <span class="hljs-string">&quot;left&quot;</span>) <span class="hljs-number">5</span>)<br><br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">handle-key</span> cd ke)    <span class="hljs-comment">;stub</span><br>  <span class="hljs-number">0</span>)<br><br><span class="hljs-comment">;&lt;template from KeyEvent&gt;</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">handle-key</span> cd ke)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">key=?</span> ke <span class="hljs-string">&quot; &quot;</span>) <span class="hljs-number">10</span>]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> cd]))<br></code></pre></td></tr></table></figure></div></details><details class="folder" index="1"><summary><p>HtDW P2 - Traffic Light 题解</p></summary><div class="body"><p><strong>预计耗时：50 min / 中等</strong></p><p>这道题让我们实现一个交通灯改变的程序，从红变成绿再变成黄，一直交替下去。如图所示：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:445/301;width:300px;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-worlds/traffic-light.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-worlds/traffic-light.webp" alt="交替交通灯" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">交替交通灯</span></div></div><p>之后就可以定义一些常量了，我们发现：</p><ul><li>每个灯的半径和间隔都是不变的</li><li>纯黑背景（与半径和间隔有关）</li><li>三个状态对应的内容（比较多）</li></ul><p>接下来就是数据定义，交通灯其实就是带有三个状态的枚举，分别是<code>&quot;red&quot;</code>、<code>&quot;yellow&quot;</code>和<code>&quot;green&quot;</code>，对应当前交通灯的颜色。由于是枚举类型所以不需要例子：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Light is one of:</span><br><span class="hljs-comment">;;  - &quot;red&quot;</span><br><span class="hljs-comment">;;  - &quot;yellow&quot;</span><br><span class="hljs-comment">;;  - &quot;green&quot;</span><br><span class="hljs-comment">;; interp. the current color of the light</span><br><span class="hljs-comment">;; &lt;examples are redundant for enumerations&gt;</span><br></code></pre></td></tr></table></figure><p>它的函数模板和规则如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs scheme">#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-light</span> l)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> l <span class="hljs-string">&quot;red&quot;</span>)    (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>        [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> l <span class="hljs-string">&quot;yellow&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>        [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> l <span class="hljs-string">&quot;green&quot;</span>)  (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]))<br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;   one of: 3 cases</span><br><span class="hljs-comment">;;   atomic distinct: &quot;red&quot;</span><br><span class="hljs-comment">;;   atomic distinct: &quot;yellow&quot;</span><br><span class="hljs-comment">;;   atomic distinct: &quot;green&quot;</span><br></code></pre></td></tr></table></figure><hr /><p>对于函数设计，这次的<code>big-bang</code>很简单，只需要<code>on-tick</code>和<code>to-draw</code>。其中<code>on-tick</code>的函数名未<code>next-color</code>，改名后：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Light -&gt; Light</span><br><span class="hljs-comment">;; called to run the animation; start with (main &quot;red&quot;)</span><br><span class="hljs-comment">;; no tests for main function</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">main</span> l)<br>  (<span class="hljs-name">big-bang</span> l<br>            (<span class="hljs-name">on-tick</span> next-color <span class="hljs-number">1</span>)   <span class="hljs-comment">; Light -&gt; Light</span><br>            (<span class="hljs-name">to-draw</span> render-light))) <span class="hljs-comment">; Light -&gt; Image</span><br></code></pre></td></tr></table></figure><p>对于<code>next-color</code>函数，它的目的是返回下一个交通灯，也是接受枚举得到枚举，写出测试、桩函数和模板复制后，完善来自模板的函数体：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Light -&gt; Light</span><br><span class="hljs-comment">;; produce next color of light</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">next-color</span> <span class="hljs-string">&quot;red&quot;</span>)    <span class="hljs-string">&quot;green&quot;</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">next-color</span> <span class="hljs-string">&quot;yellow&quot;</span>) <span class="hljs-string">&quot;red&quot;</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">next-color</span> <span class="hljs-string">&quot;green&quot;</span>)  <span class="hljs-string">&quot;yellow&quot;</span>)<br><br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">next-color</span> l)      <span class="hljs-comment">; stub</span><br>  <span class="hljs-string">&quot;red&quot;</span>)<br><span class="hljs-comment">;&lt;template from Light&gt;</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">next-color</span> l)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> l <span class="hljs-string">&quot;red&quot;</span>)    <span class="hljs-string">&quot;green&quot;</span>]<br>        [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> l <span class="hljs-string">&quot;yellow&quot;</span>) <span class="hljs-string">&quot;red&quot;</span>]<br>        [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> l <span class="hljs-string">&quot;green&quot;</span>)  <span class="hljs-string">&quot;yellow&quot;</span>]))<br></code></pre></td></tr></table></figure><p>由于我们把工作量放到了常量定义，所以渲染函数很好写，根据传入的当前灯返回对应的图像就行：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Light -&gt; Image</span><br><span class="hljs-comment">;; produce appropriate image for light color</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-light</span> <span class="hljs-string">&quot;red&quot;</span>)    RON)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-light</span> <span class="hljs-string">&quot;yellow&quot;</span>) YON)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">render-light</span> <span class="hljs-string">&quot;green&quot;</span>)  GON)<br><br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render-light</span> l)<br>  BACKGROUND)<br><span class="hljs-comment">;&lt;template from Light&gt;</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">render-light</span> l)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> l <span class="hljs-string">&quot;red&quot;</span>)    RON]<br>        [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> l <span class="hljs-string">&quot;yellow&quot;</span>) YON]<br>        [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> l <span class="hljs-string">&quot;green&quot;</span>)  GON]))<br></code></pre></td></tr></table></figure></div></details></div>]]>
    </content>
    <id>https://ziling.moe/2025/academics-ubc-cpsc-110-how-to-design-worlds/</id>
    <link href="https://ziling.moe/2025/academics-ubc-cpsc-110-how-to-design-worlds/"/>
    <published>2025-07-21T04:20:00.000Z</published>
    <summary>UBC 的计科大一必修课 - CPSC 110</summary>
    <title>UBC - CPSC 110 - How to Design Worlds</title>
    <updated>2025-07-21T04:20:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term="Academics" scheme="https://ziling.moe/categories/Academics/"/>
    <category term="UBC" scheme="https://ziling.moe/tags/UBC/"/>
    <category term="计算机科学" scheme="https://ziling.moe/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    <category term="教程" scheme="https://ziling.moe/tags/%E6%95%99%E7%A8%8B/"/>
    <content>
      <![CDATA[<p>上一章讲述了系统设计中的函数设计，但在函数设计之外，数据结构也是很重要的一部分。</p><h2 id="学习目标"><a class="markdownIt-Anchor" href="#学习目标"></a> 学习目标</h2><ul><li>能够运用《如何设计数据定义》（HtDD）方法为原子数据设计数据定义</li><li>能够识别应表示为简单原子数据、区间、枚举、分项和混合数据分项的问题领域信息</li><li>能够运用&quot;数据驱动模板&quot;方法为操作原子数据的函数生成模板</li><li>能够运用《如何设计函数》（HtDF）方法设计操作原子数据的函数</li></ul><mark class="tag-plugin colorful mark" color="warning">以下内容涉及到的edX链接均不保证可访问性</mark><h2 id="cond-expressions"><a class="markdownIt-Anchor" href="#cond-expressions"></a> cond Expressions</h2><p><code>cond</code> 表达式可以在当<emp>条件表达式</emp> <em>(Condition Expression)</em> 的分支超过2个时，简化写法。</p><p>在此之前，<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/cond-starter.rkt">下载来自edX的 cond-starter.rkt 文件</a>。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1211/771;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/cond-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/cond-starter.webp" alt="cond-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">cond-starter.rkt</span></div></div><p>让我们来过一遍这段代码在干什么：</p><ul><li>开始定义了三个不同的<code>rectangle</code>，分别命名为<code>I1</code>、<code>I2</code>、<code>I3</code></li><li>中间的三个<code>check-expect</code>先跳过，只是用来测试函数的</li><li>最后有个名为<code>aspect-ratio</code>的函数，接受一个<code>Image</code><ul><li>如果<code>img</code>的高大于<code>img</code>的宽，那么返回<code>&quot;tall&quot;</code></li><li>否则，判断<code>img</code>的高是否等于<code>img</code>的宽，如果是，返回<code>&quot;square&quot;</code><ul><li>否则返回<code>&quot;wide&quot;</code></li></ul></li></ul></li></ul><p>从函数的判断逻辑和返回值能看出来：这个函数是用来判断传入的图像是宽、是高、还是宽高相等（正方形），故它<emp>应当</emp>为并列的三个判断。但这里却通过嵌套两个<code>if</code>表达式来模拟出三个判断，这不太好。</p><p>接下来，本节会介绍<code>cond</code>表达式，它与<code>if</code>的不同点在于，<emp>它可以有多个分支</emp>。</p><details class="tag-plugin colorful folding" color="gray" open><summary><p>快捷注释</p></summary><div class="body"><p>对于大段代码，可以在它之前一行写<code>#;</code>来整个注释<emp>紧跟这一行之后的代码块</emp>。</p> <p>比如：</p> <figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs scheme">#<span class="hljs-comment">;  ; 之后整个 define 表达式就失效了</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">aspect-ratio</span> img)  <br>  (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> (<span class="hljs-name">image-height</span> img) (<span class="hljs-name">image-width</span> img))<br>      <span class="hljs-string">&quot;tall&quot;</span><br>      (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">=</span></span> (<span class="hljs-name">image-height</span> img) (<span class="hljs-name">image-width</span> img))<br>          <span class="hljs-string">&quot;square&quot;</span><br>          <span class="hljs-string">&quot;wide&quot;</span>)))<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> ...)  <span class="hljs-comment">; 这里又有效了</span><br></code></pre></td></tr></table></figure> </div></details><p>我们注释这个函数后，再写个新的，并尝试用一下新的<code>cond</code>表达式：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">aspect-ratio</span> img)<br>    (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [<span class="hljs-name">Q</span> A]  <span class="hljs-comment">; cond 表达式接受多个用[]或者()包裹的一对表达式，但通常用[]</span><br>          [<span class="hljs-name">Q</span> A]  <span class="hljs-comment">; Q 是问题，满足后得到 A，即答案</span><br>          [<span class="hljs-name">Q</span> A]  <span class="hljs-comment">; 如果前面的表达式不满足，会从上到下找到满足的</span><br>          ))<br></code></pre></td></tr></table></figure><p>这里可能会发现一个漏洞，如果<code>cond</code>里面所有的分支都没有满足，需要一个保底值，而这个保底触发条件就是<code>else</code>，所以可以尝试理解以下的改写：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">aspect-ratio</span> img)<br>    (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> (<span class="hljs-name">image-height</span> img) (<span class="hljs-name">image-width</span> img)) <span class="hljs-string">&quot;tall&quot;</span>]<br>          [(<span class="hljs-name"><span class="hljs-built_in">=</span></span> (<span class="hljs-name">image-height</span> img) (<span class="hljs-name">image-width</span> img)) <span class="hljs-string">&quot;square&quot;</span>]<br>          [<span class="hljs-name"><span class="hljs-built_in">else</span></span> <span class="hljs-string">&quot;wide&quot;</span>]  <span class="hljs-comment">; 这里如果上面两个分支都没有判断到，就执行这个</span><br>          ))<br></code></pre></td></tr></table></figure><p>当然，这个表达式的基本要求就是：</p><ul><li><code>[Q A]</code>中的<code>Q</code>始终为<code>Boolean</code>类型，因为它是用来判断能不能返回<code>A</code>的。</li><li>最后一个分支的<code>Q</code>可以是<code>else</code>。</li></ul><hr /><p>接下来让我们来解读一个完整的<code>cond</code>表达式，看看它是怎么运行的：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>) <span class="hljs-string">&quot;bigger&quot;</span>]<br>      [(<span class="hljs-name"><span class="hljs-built_in">=</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>) <span class="hljs-string">&quot;equal&quot;</span>]<br>      [(<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>) <span class="hljs-string">&quot;smaller&quot;</span>])<br></code></pre></td></tr></table></figure><p>首先，如果一个<code>cond</code>表达式里面一个<code>[Q A]</code>都没有，肯定会报错，<em>可以试试</em>。</p><p>然后，针对每一个分支，思路是：将<code>Q</code>和<code>A</code>都从表达式化简成值，比如第一行就可以变成<code>[false &quot;bigger&quot;]</code>得到：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [<span class="hljs-name">false</span> <span class="hljs-string">&quot;bigger&quot;</span>]<br>      [(<span class="hljs-name"><span class="hljs-built_in">=</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>) <span class="hljs-string">&quot;equal&quot;</span>]<br>      [(<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>) <span class="hljs-string">&quot;smaller&quot;</span>])<br></code></pre></td></tr></table></figure><p>将带有<code>false</code>的分支删掉，得到：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">=</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>) <span class="hljs-string">&quot;equal&quot;</span>]<br>      [(<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>) <span class="hljs-string">&quot;smaller&quot;</span>])<br></code></pre></td></tr></table></figure><p>然后剩下的第一行可以变成<code>[false &quot;equal&quot;]</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [<span class="hljs-name">false</span> <span class="hljs-string">&quot;equal&quot;</span>]<br>      [(<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>) <span class="hljs-string">&quot;smaller&quot;</span>])<br></code></pre></td></tr></table></figure><p>再次将带有<code>false</code>的分支删掉，得到：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> <span class="hljs-number">1</span> <span class="hljs-number">2</span>) <span class="hljs-string">&quot;smaller&quot;</span>])<br></code></pre></td></tr></table></figure><p>最后一行的<code>Q</code>为<code>true</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [<span class="hljs-name">true</span> <span class="hljs-string">&quot;smaller&quot;</span>])<br></code></pre></td></tr></table></figure><p>最终得到：<code>&quot;smaller&quot;</code>。</p><p>总结一下，<code>cond</code>表达式的解读顺序是：</p><ul><li>所有的内部的函数调用、表达式都化简</li><li>将所有的<code>Q</code>逐个变成确认的<code>true</code>/<code>false</code></li><li>将<code>cond</code>表达式变成一个准确答案</li></ul><h2 id="data-definitions"><a class="markdownIt-Anchor" href="#data-definitions"></a> Data Definitions</h2><p>在编程中，我们时常<emp>定义信息为数据</emp>，故这也是系统设计中十分重要的部分。</p><p>在此之前，<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/next-color-starter.rkt">下载来自edX的 next-color-starter.rkt 文件</a>。</p><p>假如你需要写一个程序，模拟交通灯的交替：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:765/473;width:500px;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/next-color-starter-1.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/next-color-starter-1.webp" alt="next-color-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">next-color-starter.rkt</span></div></div><p>看到这段代码可能会很困惑，除了<code>next-color</code>这个函数名看起来有点意思，这函数里面好像就只有<code>0 1 2</code>的返回值？这些测试又在干什么？</p><p>这里就要涉及到我们将实际问题抽象成所写代码的过程了。</p><p><em>ps: 我将 Problem Domain 暂且译成实际问题，而不是问题域，有兴趣可以自行搜索</em></p><p>从题目能看出，我们需要<emp>用某种方式表示</emp>，比如说红灯。</p><p><strong>红灯</strong>是<emp>信息</emp>，而它需要被<strong>代表</strong>为<emp>数据</emp>，与此同时，数据也可以<strong>解释</strong>为信息。</p><p>这里我们可以在代码中用<code>0</code>这个数来代表红灯，回到程序，我们就会合理猜测<code>1</code>或许是黄灯，<code>2</code>是绿灯。这就是<emp>数据的定义</emp>。让我们再来看看改进版：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:766/960;width:400px;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/next-color-starter-2.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/next-color-starter-2.webp" alt="next-color-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">next-color-starter.rkt</span></div></div><p>在这个程序的最开始，我们发现了它的<emp>数据定义</emp> <em>(Data Definition)</em>。之后写了它的<emp>类型注释</emp> <em>(Type Comment)</em> 描述数据是由什么形成的，即：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; TLColor is one of:</span><br><span class="hljs-comment">;;  - 0</span><br><span class="hljs-comment">;;  - 1</span><br><span class="hljs-comment">;;  - 2</span><br></code></pre></td></tr></table></figure><p>之后也写了这个数据类型的解释，表示这个数据类型与实际信息的关系：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; interp. 0 means red, 1 yellow, 2 green</span><br></code></pre></td></tr></table></figure><p>同时函数名和它的传参<code>(fn-for-tlcolor c)</code>也意味着这个函数接受<code>TLColor</code>这个类型。</p><p>然后就发现下面的函数签名变成了<code>TLColor -&gt; TLColor</code>，而非<code>Natural</code>，这意味着只有<code>0 1 2</code>这三个数是合乎条件的，其他数都不在内。</p><p>总之，数据定义描述了：</p><ul><li>如何赋予数据一个新的类型</li><li>如何让数据代表一些信息</li><li>如何将数据解释为信息</li></ul><p>同时，数据定义也让函数的：</p><ul><li>接受参数的范围受限（从<code>Natural</code>意义不明的自然数，变为自定义明确的<code>TLColor</code>）</li><li>返回值受限（同上）</li><li>帮助我们更清晰地写出测试</li></ul><p>数据定义的出现让我们不再让信息未经加工就充斥于我们的程序之中，给我们造成混乱。</p><h2 id="atomic-non-distinct"><a class="markdownIt-Anchor" href="#atomic-non-distinct"></a> Atomic Non-Distinct</h2><p>在本节开始之前，<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/city-name-starter.rkt">下载来自edX的 city-name-starter.rkt 文件</a>。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:976/348;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/city-name-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/city-name-starter.webp" alt="city-name-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">city-name-starter.rkt</span></div></div><p>在这道题中，我们需要为城市的名字这一<emp>信息</emp>设计<emp>数据定义</emp>。</p><p>假设有城市：<code>Vancouver</code>和<code>Boston</code>，这些都是信息，而且都是<emp>原子信息</emp>，即该信息不能再拆开了，里面不会有什么隐含的意味。当然，你可以说<code>Vancouver</code>可以拆成<code>V-A-N-C-O-U-V-E-R</code>多个字符，但它就不是城市的名字了。</p><p>在定义这种信息的数据时，我们可以先写一个<emp>类型注释</emp>来标注它的类型：<code>;; CityName is String</code>。借由这个类型注释，我们知道<code>Vancouver</code>的数据表示为<code>&quot;Vancouver&quot;</code>，而<code>Boston</code>则表示为<code>&quot;Boston&quot;</code>。</p><p>之后写一行解释，<code>;; interp. the name of a city</code>，说明<code>CityName</code>作为一种数据定义，代表的是城市的名字。</p><p>然后就可以写下这两个城市名字的常量了：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> CN1 <span class="hljs-string">&quot;Boston&quot;</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> CN2 <span class="hljs-string">&quot;Vancouver&quot;</span>)<br></code></pre></td></tr></table></figure><p>以及它的函数模板，标注该函数基于<code>Atomic Non-Distinct</code>规则，相关类型为<code>String</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs scheme">#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-city-name</span> cn)<br>      (<span class="hljs-name"><span class="hljs-built_in">...</span></span> cn)<br>      )<br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;   - atomic non-distinct: String</span><br></code></pre></td></tr></table></figure><p>完整的原子数据定义如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; CityName is String</span><br><span class="hljs-comment">;; interp. the name of a city</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> CN1 <span class="hljs-string">&quot;Boston&quot;</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> CN2 <span class="hljs-string">&quot;Vancouver&quot;</span>)<br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-city-name</span> cn)<br>      (<span class="hljs-name"><span class="hljs-built_in">...</span></span> cn)<br>      )<br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;   - atomic non-distinct: String</span><br></code></pre></td></tr></table></figure><h2 id="htdf-with-non-primitive-data"><a class="markdownIt-Anchor" href="#htdf-with-non-primitive-data"></a> HtDF With Non-Primitive Data</h2><p>书接上回，我们写了名为<code>CityName</code>的数据定义，也就是说<code>CityName</code>成为了一个<emp>类型</emp>。我们要为这个类型写一个函数，所以就需要回忆起上一章 HtDF recipe 的有关步骤。</p><p>该函数接受一个城市的名字，判断该城市是否为最好的城市。</p><p>我们在上一节程序的最底部写一行<code>;; Functions:</code>注释之后，定下函数的签名和目的：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; CityName -&gt; Boolean</span><br><span class="hljs-comment">;; produce true if the given city is the best in the world</span><br></code></pre></td></tr></table></figure><p>之后写一个桩函数，顺便起好函数名，比如说<code>best?</code>，接受的参数就是<code>cn</code>，随便给个返回值:</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">best?</span> cn) false)  <span class="hljs-comment">; stub</span><br></code></pre></td></tr></table></figure><p>然后就是测试，考虑到我们的返回值类型是<code>Boolean</code>，所以我们的测试一定要至少覆盖到一个<code>false</code>的结果和一个<code>true</code>的结果：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">best?</span> <span class="hljs-string">&quot;Boston&quot;</span>) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">best?</span> <span class="hljs-string">&quot;Hogsmeade&quot;</span>) true)<br></code></pre></td></tr></table></figure><p>运行后测试不通过。还记得上一节<code>CityName</code>这个类型的相关模板吗？将它复制进来，改名，并写个注释：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">; took template from CityName</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">best?</span> cn)<br>      (<span class="hljs-name"><span class="hljs-built_in">...</span></span> cn)<br>      )<br></code></pre></td></tr></table></figure><p>结合测试的情况，我们开始写函数体，通过城市的名字判断这个城市是不是最好的，我们需要一个<code>if</code>表达式：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">best?</span> cn)<br>      (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> cn <span class="hljs-string">&quot;Hogsmeade&quot;</span>)<br>            true<br>            false))<br></code></pre></td></tr></table></figure><p>运行后测试通过。</p><h2 id="htdf-x-structure-of-data-orthogonality"><a class="markdownIt-Anchor" href="#htdf-x-structure-of-data-orthogonality"></a> HtDF X Structure of Data Orthogonality</h2><p>上一节，我们体会到对于一个自定义的数据类型，我们是如何为它写函数的。本节领略一下系统设计这方面，因为设计方法本身其实也是很有说法的：</p><p>起初我们学了一些<emp>基本类型</emp> <em>(Primitive)</em>，比如说<code>String</code>、<code>Number</code>，围绕这些写了<code>double</code>、<code>yell</code>、<code>area</code>、<code>image-area</code>、<code>tall</code>等函数。</p><p>现在我们又接触了遵循 HtDD 设计的非基本类型，比如说原子类型的<code>CityName</code>以及之后会讲到的，<emp>枚举</emp> <em>(Enumeration)</em> <code>TLColor</code>。围绕它们写了<code>best?</code>、<code>next-color</code>函数。</p><p>目前没涉及到的非基本类型还有一些，除了枚举之外，还有<code>distinct</code>、<code>interval</code>和<code>itemization</code>。</p><p>这两大种类型还能互相结合，变成<code>Compound</code>类型，还有<code>list</code>、<code>tree</code>之类。</p><p>即使之后学到的数据类型越来越多，HtDF recipe 依然适用，它保证涉及到得到数据形式都是<emp>正交的</emp> <em>(Orthogonal)</em>。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1304/565;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/orthogonality.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/orthogonality.webp" alt="Data Types" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">Data Types</span></div></div><p>数据类型和数据结构都是本章重点探讨的话题，故之后将会在数据上花更多时间。</p><h2 id="interval"><a class="markdownIt-Anchor" href="#interval"></a> Interval</h2><p>在本节开始之前，<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/seat-num-starter.rkt">下载来自edX的 seat-num-starter.rkt 文件</a>。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1088/360;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/seat-num-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/seat-num-starter.webp" alt="seat-num-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">seat-num-starter.rkt</span></div></div><p>在一个矩形的剧院里管理售票，为此设计一个数据定义来表示一排中座位号，一排有32个位置。</p><p>回忆起<code>atomic</code>相关的内容，座位号确实可以代指一个<code>Integer</code>类型，但它又不仅仅是个简单的<code>Integer</code>，而是由范围的。</p><p>我们可以用<emp>区间</emp> <em>(Interval)</em> 可以代指带有范围的数，比如说<code>Integer[0, 10]</code>是指一个包含<code>0</code>和<code>10</code>的正整数区间，<code>Number[0, 10)</code>则是指不包含<code>10</code>的数，<em>这里开闭区间的表示和数学一样</em>。</p><p>接下来开始写这道题的数据定义：<code>;; SeatNum is Integer[1, 32]</code>，当然考虑到自然数的概念，写<code>;; SeatNum is Natural[1, 32]</code>也是可以的，这里使用<code>Natural</code>类型。</p><p>下一行就是解释这个区间，同时注明范围：<code>;; interp. seat number in a row, 1 and 32 are aisle seats</code>。</p><p>然后为这个区间写一些常量作为例子：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> SN1 <span class="hljs-number">1</span>)   <span class="hljs-comment">; aisle</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> SN2 <span class="hljs-number">12</span>)  <span class="hljs-comment">; middle</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> SN3 <span class="hljs-number">13</span>)  <span class="hljs-comment">; aisle</span><br></code></pre></td></tr></table></figure><p>以及<code>SeatNum</code>相关函数的模板，并标注该函数基于<code>Atomic Non-Distinct</code>规则，相关类型为<code>Natural[1, 32]</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-seat-num</span> sn)<br>      (<span class="hljs-name"><span class="hljs-built_in">...</span></span> sn))<br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;   - atomic non-distinct: Natural[1, 32]</span><br></code></pre></td></tr></table></figure><p>完整的<code>Interval</code>原子数据定义如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; SeatNum is Integer[1, 32]</span><br><span class="hljs-comment">;; interp. seat number in a row, 1 and 32 are aisle seats</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> SN1 <span class="hljs-number">1</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> SN2 <span class="hljs-number">12</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> SN3 <span class="hljs-number">13</span>)<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-seat-num</span> sn)<br>      (<span class="hljs-name"><span class="hljs-built_in">...</span></span> sn))<br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;   - atomic non-distinct: Natural[1, 32]</span><br></code></pre></td></tr></table></figure><h2 id="enumeration"><a class="markdownIt-Anchor" href="#enumeration"></a> Enumeration</h2><p>在本节开始之前，<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/letter-grade-starter.rkt">下载来自edX的 letter-grade-starter.rkt 文件</a>。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:976/305;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/letter-grade-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/letter-grade-starter.webp" alt="letter-grade-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">letter-grade-starter.rkt</span></div></div><p>这道题让我们设计一个数据类型来代表学生的成绩 <em>(A, B, C)</em>，<emp>它包含三个不同的等级</emp>，我们可以使用<emp>枚举</emp> <em>(Enumeration)</em> 来代指成绩。枚举还可以用于，比如说红绿灯的状态只有三种可能：<code>red</code>、<code>yellow</code>、<code>green</code>。</p><p>枚举的数据定义不太一样，我们先给<strong>成绩</strong>起个名字，同时声明它是三个字母中的一个：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; LetterGrade is one of:</span><br><span class="hljs-comment">;;  - &quot;A&quot;</span><br><span class="hljs-comment">;;  - &quot;B&quot;</span><br><span class="hljs-comment">;;  - &quot;C&quot;</span><br></code></pre></td></tr></table></figure><p>由于枚举中的各个项通常是字符串，它的解释可以<emp>很直接</emp>：<code>;; interp. the letter grade in a course</code>。</p><p>在之前，为了代表三个项，我们可能需要用数字<code>0</code>、<code>1</code>、<code>2</code>等代替，一一去写每个数代表的意思，但现在可以通过枚举和里面的字符串来简化解释。</p><p>之后对应这个枚举写三个常量作为例子：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LG1 <span class="hljs-string">&quot;A&quot;</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LG2 <span class="hljs-string">&quot;B&quot;</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LG3 <span class="hljs-string">&quot;C&quot;</span>)<br></code></pre></td></tr></table></figure><p>Wait，最开始的类型定义很清晰地指出了<code>LetterGrade</code>和三个的等级之间的关系，那这些例子完全是重复的，所以我们完全可以这么写：<code>&lt;examples are redundant for enumerations&gt;</code></p><p>为这个枚举类型写一个模板函数，注明它使用的模板规则。枚举的模板规则比较复杂，<code>enumeration</code>类型需要在模板内写一个<code>cond</code>表达式来覆盖所有可能的枚举值，以及在规则内需要写上多少个 cases 以及对应的每个 case。</p><p>在这里三个项不是以前遇到的<code>atomic non-distinct</code>这种原子数据类型，而是三个确切原子类型的值，所以需要写<code>atomic distinct</code>。</p><p>对于每个<code>atomic distinct</code>，我们要根据它的类型去完善<code>cond</code>表达式里面的<code>Q</code>，比如说这里的<code>String</code>，在<code>Q</code>的位置就得填：<code>(string=? lg &quot;A&quot;)</code>。之后<code>A</code>的位置就是对应 case 的处理了。</p><p>按照以往，模板函数的返回值应该是诸如<code>(... s)</code>一类，但在这里，<code>A</code>应该填<code>(...)</code>就行了</p><p>以下是枚举类型的完整数据定义：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; LetterGrade is one of:</span><br><span class="hljs-comment">;;  - &quot;A&quot;</span><br><span class="hljs-comment">;;  - &quot;B&quot;</span><br><span class="hljs-comment">;;  - &quot;C&quot;</span><br><span class="hljs-comment">;; interp. the letter grade in a course</span><br><span class="hljs-comment">;; &lt;examples are redundant for enumerations&gt;</span><br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-letter-grade</span> lg)<br>      (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> lg <span class="hljs-string">&quot;A&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>            [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> lg <span class="hljs-string">&quot;B&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>            [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> lg <span class="hljs-string">&quot;C&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]))<br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;  - one of: 3 cases</span><br><span class="hljs-comment">;;  - atomic distinct: &quot;red&quot;</span><br><span class="hljs-comment">;;  - atomic distinct: &quot;yellow&quot;</span><br><span class="hljs-comment">;;  - atomic distinct: &quot;green&quot;</span><br></code></pre></td></tr></table></figure><details class="tag-plugin colorful folding" color="gray" open><summary><p>补充</p></summary><div class="body"><p>由于 edX 有很多参考难以说完，这里列举一些其他的：</p> <p>Atomic Non-Distinct, 返回均为<code>(... x)</code>:</p> <ul> <li><code>Number</code> -&gt; <code>(number? x)</code></li> <li><code>String</code> -&gt; <code>(string? x)</code></li> <li><code>Boolean</code> -&gt; <code>(boolean? x)</code></li> <li><code>Image</code> -&gt; <code>(image? x)</code></li> <li>interval like <code>Number[0, 10)</code> -&gt; <code>(and (&lt;= 0 x) (&lt; x 10))</code></li> </ul> <p>Atomic Distinct Value, 返回均为<code>(...)</code>:</p> <ul> <li><code>&quot;red&quot;</code> -&gt; <code>(string=? x &quot;red&quot;)</code></li> <li><code>false</code> -&gt; <code>(false? x)</code></li> <li><code>empty</code> -&gt; <code>(empty? x)</code></li> </ul> <p><code>Enumeration</code>和下一节<code>Itemization</code>都需要在模板函数体内体现<code>cond</code>表达式</p> </div></details><h2 id="itemization"><a class="markdownIt-Anchor" href="#itemization"></a> Itemization</h2><p>在本节开始之前，<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/countdown-starter.rkt">下载来自edX的 countdown-starter.rkt 文件</a>。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:932/414;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/countdown-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/countdown-starter.webp" alt="countdown-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">countdown-starter.rkt</span></div></div><p>我们先站在枚举的视角来看这个问题，会发现它和上一道题一样，把情况分为三类：</p><ul><li>还没开始</li><li>倒数10秒</li><li>Happy New Year!</li></ul><p>但仔细一想不太对，第一和第三个状态可以轻松表示，但第二个这个倒数十秒似乎不简单，我们暂且用<strong>区间</strong>来表示。</p><p>这就是本节介绍的<emp>分项</emp> <em>(Itemization)</em>，它适用于其中有项并不只是简单的基本类型的场景，也可以理解为枚举的加强版。</p><p><em>ps: 奇怪的翻译</em></p><p>和枚举一样，我们来写它的数据定义：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; CountDown is one of:</span><br><span class="hljs-comment">;;  - false</span><br><span class="hljs-comment">;;  - Natural[1, 10]</span><br><span class="hljs-comment">;;  - &quot;complete&quot; </span><br></code></pre></td></tr></table></figure><p>但不同于枚举，几个项可以被一行简单解释，分项的解释需要针对于每一项：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; interp.</span><br><span class="hljs-comment">;;    false            means countdown has not yet started</span><br><span class="hljs-comment">;;    Natural[1, 10]   means countdown is running and how many seconds left</span><br><span class="hljs-comment">;;    &quot;complete&quot;       means countdown is over</span><br></code></pre></td></tr></table></figure><p>也因为这一点，它的例子不能省略：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> CD1 false)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> CD2 <span class="hljs-number">10</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> CD3 <span class="hljs-number">1</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> CD4 <span class="hljs-string">&quot;complete&quot;</span>)<br></code></pre></td></tr></table></figure><p>以及它的函数模板：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs scheme">#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-countdown</span> c)<br>      (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">false?</span> c) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>            [(<span class="hljs-name"><span class="hljs-built_in">and</span></span> (<span class="hljs-name"><span class="hljs-built_in">number?</span></span> c) (<span class="hljs-name"><span class="hljs-built_in">&lt;=</span></span> <span class="hljs-number">1</span> c) (<span class="hljs-name"><span class="hljs-built_in">&lt;=</span></span> c <span class="hljs-number">10</span>)) (<span class="hljs-name"><span class="hljs-built_in">...</span></span> c)]<br>            [<span class="hljs-name"><span class="hljs-built_in">else</span></span> (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]))<br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;   - one of: 3 cases</span><br><span class="hljs-comment">;;   - atomic distinct: false</span><br><span class="hljs-comment">;;   - atomic non-distinct: Natural[1, 10]</span><br><span class="hljs-comment">;;   - atomic distinct: &quot;complete&quot;</span><br></code></pre></td></tr></table></figure><details class="tag-plugin colorful folding" color="blue" open><summary><p>类型安全</p></summary><div class="body"><p>也许会疑惑为什么<code>(and (number? c) (&lt;= 1 c) (&lt;= c 10))</code>需要有个<code>(number? c)</code>，做三个与判断，这是因为每个<code>Q</code>都需要将你传入值的类型判断出来，这样就能分情况处理。</p> <p>而且，最关键的是这个<code>and</code>的后面两个参数都是只有数字才能做的判断，不能把其他类型放进去比大小，所以应该在第一个参数这写一个数字类型判断，防止程序出问题。</p> </div></details><h2 id="htdf-with-interval"><a class="markdownIt-Anchor" href="#htdf-with-interval"></a> HtDF with Interval</h2><p>包括本节，之后两节将会通过写函数的方式巩固对<code>Interval</code>、<code>Enumeration</code>和<code>Itemization</code>的理解。</p><p>在本节开始之前，<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/aisle-starter.rkt">下载来自edX的 aisle-starter.rkt 文件</a>。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:908/831;width:600px;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/aisle-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/aisle-starter.webp" alt="aisle-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">aisle-starter.rkt</span></div></div><p>这道题让我们设计一个用于代表<code>SeatNum</code>的数据定义，如果给定的座位号在过道上，就返回<code>true</code>。让我们直接从代码文件的<code>;; Functions:</code>一栏开始写函数：</p><p>回忆 HtDF recipe，我们需要先写出函数的签名：<code>SeatNum -&gt; Boolean</code>，因为是从座位号这个自定义类型得到一个布尔值，之后再解释：<code>produce true is the given seat number is on the aisle</code>。</p><p>桩函数也就能写出来了：<code>(define (aisle? an) false)  ; stub</code></p><p>从代码最开始的数据定义能看出来<code>SeatNum</code>是一个<code>Natural[1, 32]</code>，它特别是闭区间，其中<code>1</code>和<code>32</code>都是属于过道的座位。所以我们最好写出三个测试，分别是<code>1</code>、中间的某个位置和<code>32</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">aisle?</span> <span class="hljs-number">1</span>) true)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">aisle?</span> <span class="hljs-number">16</span>) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">aisle?</span> <span class="hljs-number">32</span>) true)<br></code></pre></td></tr></table></figure><p>之后让我们从代码文件上方给出的有关<code>SeatNum</code>类型的函数模板，声明<code>;&lt;use template from SeatNum&gt;</code>，改名为<code>aisle?</code>，得到：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">aisle?</span> sn)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> sn)) <br></code></pre></td></tr></table></figure><p><em>ps: 注意把桩函数注释掉</em></p><p>然后开始实现函数体。我们可以从题目思考下，首先是不是只要座位号为<code>1</code>或者<code>32</code>就说明它在过道上，也就是说我们需要判断座位号是否是这两个数中的一个。这里可以想出以下可能方案：</p><div class="tag-plugin tabs" align="center"id="tab_6"><div class="nav-tabs"><div class="tab active"><a href="#tab_6-1">if 表达式</a></div><div class="tab"><a href="#tab_6-2">cond 表达式</a></div><div class="tab"><a href="#tab_6-3">or 表达式</a></div></div><div class="tab-content"><div class="tab-pane active" id="tab_6-1"><p>可以通过判断<code>sn</code>是<code>1</code>或<code>32</code>来返回<code>true</code>和<code>false</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">aisle?</span> sn)<br>  (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">or</span></span> (<span class="hljs-name"><span class="hljs-built_in">=</span></span> sn <span class="hljs-number">1</span>) (<span class="hljs-name"><span class="hljs-built_in">=</span></span> sn <span class="hljs-number">32</span>))<br>      true<br>      false))<br></code></pre></td></tr></table></figure></div><div class="tab-pane" id="tab_6-2"><p>可以将<code>1</code>、<code>32</code>和其他情况视为<code>cond</code>表达式的三个 cases：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">aisle?</span> sn)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">=</span></span> sn <span class="hljs-number">1</span>) true]<br>        [(<span class="hljs-name"><span class="hljs-built_in">=</span></span> sn <span class="hljs-number">32</span>) true]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> false])) <br></code></pre></td></tr></table></figure></div><div class="tab-pane" id="tab_6-3"><p>这是最简单的，因为我们知道<code>or</code>表达式的返回值就是布尔：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">aisle?</span> sn)<br>  (<span class="hljs-name"><span class="hljs-built_in">or</span></span> (<span class="hljs-name"><span class="hljs-built_in">=</span></span> sn <span class="hljs-number">1</span>) (<span class="hljs-name"><span class="hljs-built_in">=</span></span> sn <span class="hljs-number">32</span>))) <br></code></pre></td></tr></table></figure></div></div></div><p>函数部分代码如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Functions:</span><br><span class="hljs-comment">;; SeatNum -&gt; Boolean</span><br><span class="hljs-comment">;; produce true is the given seat number is on the aisle</span><br><br><span class="hljs-comment">;(define (aisle? an) false)  ; stub</span><br><span class="hljs-comment">;&lt;use template from SeatNum&gt;</span><br><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">aisle?</span> <span class="hljs-number">1</span>) true)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">aisle?</span> <span class="hljs-number">16</span>) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">aisle?</span> <span class="hljs-number">32</span>) true)<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">aisle?</span> sn)<br>  (<span class="hljs-name"><span class="hljs-built_in">or</span></span> (<span class="hljs-name"><span class="hljs-built_in">=</span></span> sn <span class="hljs-number">1</span>) (<span class="hljs-name"><span class="hljs-built_in">=</span></span> sn <span class="hljs-number">32</span>))) <br></code></pre></td></tr></table></figure><h2 id="htdf-with-enumeration"><a class="markdownIt-Anchor" href="#htdf-with-enumeration"></a> HtDF with Enumeration</h2><p>在本节开始之前，<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/bump-up-starter.rkt">下载来自edX的 bump-up-starter.rkt 文件</a>。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:991/1072;width:600px;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/bump-up-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/bump-up-starter.webp" alt="bump-up-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">bump-up-starter.rkt</span></div></div><p>先过一眼题目：设计一个与<code>LetterGrade</code>数据类型相关的函数，接受一个<code>LetterGrade</code>，得到它的下一个更高等级，比如<code>C</code>变<code>B</code>、<code>B</code>变<code>A</code>，当然<code>A</code>是不变的。</p><p>由此可知函数签名：<code>LetterGrade -&gt; LetterGrade</code>，解释为<code>produce next highest letter grade (no change for A)</code>。</p><p>为此写桩函数：<code>(define (bump-up lg) &quot;A&quot;)  ; stub</code>。</p><p>考虑到代码文件上方的<code>LetterGrade</code>数据类型是一个枚举，我们在写测试的时候需要照顾到所有的情况，故我们需要写三个测试：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">bump-up</span> <span class="hljs-string">&quot;A&quot;</span>) <span class="hljs-string">&quot;A&quot;</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">bump-up</span> <span class="hljs-string">&quot;B&quot;</span>) <span class="hljs-string">&quot;A&quot;</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">bump-up</span> <span class="hljs-string">&quot;C&quot;</span>) <span class="hljs-string">&quot;B&quot;</span>)<br></code></pre></td></tr></table></figure><p>之后声明<code>;&lt;use template from LetterGrade&gt;</code>使用上面写的<code>LetterGrade</code>模板函数，将它复制过来，并改名为<code>bump-up</code>，由于函数做的事情很简单，顺手就可以把函数体写了：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">bump-up</span> lg)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> lg <span class="hljs-string">&quot;A&quot;</span>) <span class="hljs-string">&quot;A&quot;</span>]<br>        [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> lg <span class="hljs-string">&quot;B&quot;</span>) <span class="hljs-string">&quot;A&quot;</span>]<br>        [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> lg <span class="hljs-string">&quot;C&quot;</span>) <span class="hljs-string">&quot;B&quot;</span>]))<br></code></pre></td></tr></table></figure><p>测试运行通过，函数部分代码如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Functions:</span><br><span class="hljs-comment">;; LetterGrade -&gt; LetterGrade</span><br><span class="hljs-comment">;; produce next highest letter grade (no change for A)</span><br><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">bump-up</span> <span class="hljs-string">&quot;A&quot;</span>) <span class="hljs-string">&quot;A&quot;</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">bump-up</span> <span class="hljs-string">&quot;B&quot;</span>) <span class="hljs-string">&quot;A&quot;</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">bump-up</span> <span class="hljs-string">&quot;C&quot;</span>) <span class="hljs-string">&quot;B&quot;</span>)<br><br><span class="hljs-comment">;(define (bump-up lg) &quot;A&quot;)  ; stub</span><br><span class="hljs-comment">;&lt;use template from LetterGrade&gt;</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">bump-up</span> lg)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> lg <span class="hljs-string">&quot;A&quot;</span>) <span class="hljs-string">&quot;A&quot;</span>]<br>        [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> lg <span class="hljs-string">&quot;B&quot;</span>) <span class="hljs-string">&quot;A&quot;</span>]<br>        [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> lg <span class="hljs-string">&quot;C&quot;</span>) <span class="hljs-string">&quot;B&quot;</span>]))<br></code></pre></td></tr></table></figure><h2 id="htdf-with-itemization"><a class="markdownIt-Anchor" href="#htdf-with-itemization"></a> HtDF with Itemization</h2><p>在本节开始之前，<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/countdown-to-display-starter.rkt">下载来自edX的 countdown-to-display-starter.rkt 文件</a>，并在代码文件上方引入<code>(require 2htdp/image)</code></p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1056/1210;width:600px;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/countdown-to-display-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/countdown-to-display-starter.webp" alt="countdown-to-display-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">countdown-to-display-starter.rkt</span></div></div><p>题目下方的数据定义在之前介绍分项的时候写过了，我们要做的是：设计一个函数，接受<code>Countdown</code>类型，得到能显示当前状态的图像。</p><p>由此可知函数签名：<code>Countdown -&gt; Image</code>，解释为<code>produce nice image of current state of countdown</code>。</p><p>为此写桩函数：<code>(define (countdown-to-image c) (square 0 &quot;solid&quot; &quot;white&quot;))  ; stub</code>。</p><p>考虑到代码文件上方的<code>Countdown</code>数据类型是分项，我们在写测试的时候需要照顾到所有的情况，故我们需要写三个测试，后面的图像表达可以自定义：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">countdown-to-image</span> false) (<span class="hljs-name">square</span> <span class="hljs-number">0</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;white&quot;</span>))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">countdown-to-image</span> <span class="hljs-number">5</span>) (<span class="hljs-name">text</span> (<span class="hljs-name"><span class="hljs-built_in">number-&gt;string</span></span> <span class="hljs-number">5</span>) <span class="hljs-number">24</span> <span class="hljs-string">&quot;black&quot;</span>))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">countdown-to-image</span> <span class="hljs-string">&quot;complete&quot;</span>) (<span class="hljs-name">text</span> <span class="hljs-string">&quot;Happy New Year!!!&quot;</span> <span class="hljs-number">24</span> <span class="hljs-string">&quot;red&quot;</span>))<br></code></pre></td></tr></table></figure><details class="tag-plugin colorful folding" color="blue" open><summary><p>text 表达式</p></summary><div class="body"><p>这里出现了一个属于图像库的新表达式，强烈建议在了解所有新表达式的时候先去官方文档查询用法。</p> <p>这里样例内的<code>text</code>表达式需要传入一个<code>String</code>、一个<code>Number</code>和一个<code>String</code>，它们分别对应：</p> <ul> <li>你要显示的文字</li> <li>字号大小</li> <li>文字颜色</li> </ul> </div></details><p>之后声明<code>;&lt;use template from Countdown&gt;&gt;</code>使用上面写的<code>Countdown&gt;</code>模板函数，将它复制过来，并改名为<code>countdown-to-image</code>，由于函数做的事情很简单，顺手就可以把函数体写了：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">countdown-to-image</span> c)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">false?</span> c) (<span class="hljs-name">square</span> <span class="hljs-number">0</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;white&quot;</span>)]<br>        [(<span class="hljs-name"><span class="hljs-built_in">and</span></span> (<span class="hljs-name"><span class="hljs-built_in">number?</span></span> c) (<span class="hljs-name"><span class="hljs-built_in">&lt;=</span></span> <span class="hljs-number">1</span> c) (<span class="hljs-name"><span class="hljs-built_in">&lt;=</span></span> c <span class="hljs-number">10</span>)) (<span class="hljs-name">text</span> (<span class="hljs-name"><span class="hljs-built_in">number-&gt;string</span></span> <span class="hljs-number">5</span>) <span class="hljs-number">24</span> <span class="hljs-string">&quot;black&quot;</span>)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> (<span class="hljs-name">text</span> <span class="hljs-string">&quot;Happy New Year!!!&quot;</span> <span class="hljs-number">24</span> <span class="hljs-string">&quot;red&quot;</span>)]))<br></code></pre></td></tr></table></figure><p>测试运行通过，函数部分代码如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Functions:</span><br><span class="hljs-comment">;; Countdown -&gt; Image</span><br><span class="hljs-comment">;; produce nice image of current state of countdown</span><br><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">countdown-to-image</span> false) (<span class="hljs-name">square</span> <span class="hljs-number">0</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;white&quot;</span>))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">countdown-to-image</span> <span class="hljs-number">5</span>) (<span class="hljs-name">text</span> (<span class="hljs-name"><span class="hljs-built_in">number-&gt;string</span></span> <span class="hljs-number">5</span>) <span class="hljs-number">24</span> <span class="hljs-string">&quot;black&quot;</span>))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">countdown-to-image</span> <span class="hljs-string">&quot;complete&quot;</span>) (<span class="hljs-name">text</span> <span class="hljs-string">&quot;Happy New Year!!!&quot;</span> <span class="hljs-number">24</span> <span class="hljs-string">&quot;red&quot;</span>))<br><br><span class="hljs-comment">;(define (countdown-to-image c) (square 0 &quot;solid&quot; &quot;white&quot;))  ; stub</span><br><span class="hljs-comment">;&lt;use template from Countdown&gt;</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">countdown-to-image</span> c)<br>  (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name">false?</span> c) (<span class="hljs-name">square</span> <span class="hljs-number">0</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;white&quot;</span>)]<br>        [(<span class="hljs-name"><span class="hljs-built_in">and</span></span> (<span class="hljs-name"><span class="hljs-built_in">number?</span></span> c) (<span class="hljs-name"><span class="hljs-built_in">&lt;=</span></span> <span class="hljs-number">1</span> c) (<span class="hljs-name"><span class="hljs-built_in">&lt;=</span></span> c <span class="hljs-number">10</span>)) (<span class="hljs-name">text</span> (<span class="hljs-name"><span class="hljs-built_in">number-&gt;string</span></span> <span class="hljs-number">5</span>) <span class="hljs-number">24</span> <span class="hljs-string">&quot;black&quot;</span>)]<br>        [<span class="hljs-name"><span class="hljs-built_in">else</span></span> (<span class="hljs-name">text</span> <span class="hljs-string">&quot;Happy New Year!!!&quot;</span> <span class="hljs-number">24</span> <span class="hljs-string">&quot;red&quot;</span>)]))<br></code></pre></td></tr></table></figure><h2 id="structure-of-information-flows-through"><a class="markdownIt-Anchor" href="#structure-of-information-flows-through"></a> Structure of Information Flows Through</h2><p>回忆一下，在本章开始时，有一张讲述数据结构与关系的图：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1304/565;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/orthogonality.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-data/orthogonality.webp" alt="Data Types" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">Data Types</span></div></div><p>从刚刚学的三个新的数据类型能看出来它不仅在简单组合原子数据类型，更是在阐述一个十分完整的数据定义过程：从<strong>信息</strong>到<strong>数据</strong>，再到<strong>模板</strong>和<strong>测试</strong>。</p><p>在程序设计中，了解信息本身的结构是至关重要的部分，因为如果数据定义变得越来越复杂，一个巧妙的结构设计就能让整个程序都变得简洁有效。</p><h2 id="practice-problems"><a class="markdownIt-Anchor" href="#practice-problems"></a> Practice Problems</h2><p>这一章的 Recommended Problems:</p><ul><li>HtDD P2 - Demolish<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/demolish-starter.rkt">demolish-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/demolish-solution.rkt">demolish-solution.rkt</a></li></ul></li><li>HtDD P3 - Rocket Descent<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/rocket-starter.rkt">rocket-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/rocket-solution.rkt">rocket-solution.rkt</a></li></ul></li></ul><p><em>ps: 我觉得难度在于背模板，而不是写代码本身</em></p><div class="tag-plugin colorful folders" ><details class="folder" index="0"><summary><p>HtDD P2 - Demolish 题解</p></summary><div class="body"><p><strong>预计耗时：20 min / 中等</strong></p><p>这道题有两个部分，分别是数据定义和函数设计。让我们先来看数据定义部分：你需要定义一个<code>BuildingStatus</code>类型来将温哥华的建筑物分为<code>&quot;new&quot;</code>、<code>&quot;old&quot;</code>和<code>&quot;heritage&quot;</code>。</p><p>结合之前学的类型，我们可以使用<strong>枚举</strong>，并写上这个类型的意思。当然，由于它是枚举类型，例子是不需要的：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; BuildingStatus is one of:</span><br><span class="hljs-comment">;;   - &quot;new&quot;</span><br><span class="hljs-comment">;;   - &quot;old&quot;</span><br><span class="hljs-comment">;;   - &quot;heritage&quot;</span><br><span class="hljs-comment">;; interp. the classification level of buildings in city</span><br><span class="hljs-comment">;; &lt;examples are reduntant for enumerations&gt;</span><br></code></pre></td></tr></table></figure><p>因为它是枚举，所以在设计函数模板时需要用<code>cond</code>表达式来囊括所有情况，同时将每个 case 的<code>Q</code>完善：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-building-status</span> bs)<br>      (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> bs <span class="hljs-string">&quot;new&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>            [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> bs <span class="hljs-string">&quot;old&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>            [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> bs <span class="hljs-string">&quot;heritage&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]))<br></code></pre></td></tr></table></figure><p>以及模板的规则，因为枚举的三个都是<code>String</code>，而<code>String</code>属于<code>Atomic Distinct</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;  - one of: 3 cases</span><br><span class="hljs-comment">;;  - atomic distinct: &quot;new&quot;</span><br><span class="hljs-comment">;;  - atomic distinct: &quot;old&quot;</span><br><span class="hljs-comment">;;  - atomic distinct: &quot;heritage&quot;</span><br></code></pre></td></tr></table></figure><p>数据定义部分代码：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; BuildingStatus is one of:</span><br><span class="hljs-comment">;;   - &quot;new&quot;</span><br><span class="hljs-comment">;;   - &quot;old&quot;</span><br><span class="hljs-comment">;;   - &quot;heritage&quot;</span><br><span class="hljs-comment">;; interp. the classification level of buildings in city</span><br><span class="hljs-comment">;; &lt;examples are reduntant for enumerations&gt;</span><br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-building-status</span> bs)<br>      (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> bs <span class="hljs-string">&quot;new&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>            [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> bs <span class="hljs-string">&quot;old&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]<br>            [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> bs <span class="hljs-string">&quot;heritage&quot;</span>) (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]))<br><br><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;  - one of: 3 cases</span><br><span class="hljs-comment">;;  - atomic distinct: &quot;new&quot;</span><br><span class="hljs-comment">;;  - atomic distinct: &quot;old&quot;</span><br><span class="hljs-comment">;;  - atomic distinct: &quot;heritage&quot;</span><br></code></pre></td></tr></table></figure><hr /><p>数据定义部分结束，接下来是函数设计：判断传入的城市状态是否为<code>&quot;old&quot;</code>，如果是就应被拆除，否则不拆，函数名为<code>demolish</code>。</p><p>从题目能理解出它的传入值类型就是刚刚的<code>BuildingStatus</code>，而返回值类型是<code>Boolean</code>。同时函数的目的就是：<code>produce true if a building should be torn down</code>。</p><p>考虑到该枚举类型存在三种情况，我们需要写三个<code>check-expect</code>，除了<code>&quot;old&quot;</code>情况，返回值都是<code>false</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">demolish</span> <span class="hljs-string">&quot;new&quot;</span>) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">demolish</span> <span class="hljs-string">&quot;old&quot;</span>) true)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">demolish</span> <span class="hljs-string">&quot;heritage&quot;</span>) false)<br></code></pre></td></tr></table></figure><p>定义它的桩函数：<code>;(define (demolish bs) true)  ; stub</code>，并且声明将会用<code>BuildingStatus</code>的函数模板：<code>;&lt;use template from BuildingStatus&gt;</code></p><p>接下来将数据定义部分的函数模板复制下来，将函数名改为<code>demolish</code>，并且根据测试来完善函数体：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">demolish</span> bs)<br>      (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> bs <span class="hljs-string">&quot;new&quot;</span>) false]<br>            [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> bs <span class="hljs-string">&quot;old&quot;</span>) true]<br>            [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> bs <span class="hljs-string">&quot;heritage&quot;</span>) false]))<br></code></pre></td></tr></table></figure><p>函数设计代码如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; BuildingStatus -&gt; Boolean</span><br><span class="hljs-comment">;; produce true if a building should be torn down</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">demolish</span> <span class="hljs-string">&quot;new&quot;</span>) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">demolish</span> <span class="hljs-string">&quot;old&quot;</span>) true)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">demolish</span> <span class="hljs-string">&quot;heritage&quot;</span>) false)<br><br><span class="hljs-comment">;(define (demolish bs) true)  ; stub</span><br><span class="hljs-comment">;&lt;use template from BuildingStatus&gt;</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">demolish</span> bs)<br>      (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> bs <span class="hljs-string">&quot;new&quot;</span>) false]<br>            [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> bs <span class="hljs-string">&quot;old&quot;</span>) true]<br>            [(<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> bs <span class="hljs-string">&quot;heritage&quot;</span>) false]))<br></code></pre></td></tr></table></figure></div></details><details class="folder" index="1"><summary><p>HtDD P3 - Rocket Descent 题解</p></summary><div class="body"><p><strong>预计耗时：25 min / 困难</strong></p><p>这道题也是相同的两个部分。让我们先来看数据定义部分：你需要定义一个<code>RocketDescent</code>类型来记录火箭的轨迹。火箭是从第100公里从地球降落，到达地面视为降落完成。</p><p>由于我们需要记录轨迹本身，即<code>Number(0, 100]</code>，和降落完成状态<code>&quot;done&quot;</code>。以此考虑使用<strong>分项</strong>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; RocketDescent is one of:</span><br><span class="hljs-comment">;;   - Number(0, 100]</span><br><span class="hljs-comment">;;   - &quot;done&quot;</span><br></code></pre></td></tr></table></figure><p><em>ps: <code>Number[0, 100]</code> 也是可以的</em></p><p><em>ps2: 教授这里将降落完成状态视为<code>false</code>，我觉得怪怪的，用了<code>&quot;done&quot;</code></em></p><p>因为这是分项，我们需要解释每一个情况，以及例子：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; interp.</span><br><span class="hljs-comment">;;   Number(0, 100]  means rocket is descending 100 kilometers to Earth</span><br><span class="hljs-comment">;;   &quot;done&quot;          means rocket is landed</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> RD1 <span class="hljs-number">100</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> RD2 <span class="hljs-number">0.5</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> RD3 <span class="hljs-string">&quot;done&quot;</span>)<br></code></pre></td></tr></table></figure><p>分项的函数模板也是用<code>cond</code>表达式列举情况，对于第一个情况的<code>Q</code>可以认为是：在判断传入类型是<code>Number</code>的前提下，判断它是否在<code>(0, 100]</code>之间：<code>(and (number? rd) (&lt; 0 rd) (&lt;= rd 100))</code>。第二个<code>Q</code>就可以使用<code>else</code>简写：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">fn-for-rocket-descent</span> rd)<br>      (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">and</span></span> (<span class="hljs-name"><span class="hljs-built_in">number?</span></span> rd) (<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> <span class="hljs-number">0</span> rd) (<span class="hljs-name"><span class="hljs-built_in">&lt;=</span></span> rd <span class="hljs-number">100</span>)) (<span class="hljs-name"><span class="hljs-built_in">...</span></span> rd)]<br>            [<span class="hljs-name"><span class="hljs-built_in">else</span></span> (<span class="hljs-name"><span class="hljs-built_in">...</span></span>)]))<br></code></pre></td></tr></table></figure><p>声明模板规则使用时需注意<code>Interval</code>类型是<code>Atomic Non-distinct</code>的：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Template rules used:</span><br><span class="hljs-comment">;;   - one of: 2 cases</span><br><span class="hljs-comment">;;   - atomic non-distinct: Number(0, 100]</span><br><span class="hljs-comment">;;   - atomic distinct: &quot;done&quot;</span><br></code></pre></td></tr></table></figure><hr /><p>数据定义部分结束，接下来是函数设计：将火箭降落状态变成可读的字符串，在降落中输出剩余距离，在降落时输出<code>&quot;The rocket has landed!&quot;</code>，函数名是<code>rocket-descent-to-msg</code>。</p><p>从题目能理解出它的传入值类型就是刚刚的<code>RocketDescent</code>，而返回值类型是<code>String</code>。同时函数的目的就是：<code>produce a broadcast message of rocket's descent distance</code>。</p><p>考虑到该分项类型存在两种情况，且里面有<code>Interval</code>，故会有多个<code>check-expect</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">rocket-descent-to-msg</span> <span class="hljs-number">100</span>) <span class="hljs-string">&quot;Altitude is 100 kms.&quot;</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">rocket-descent-to-msg</span> <span class="hljs-number">0.5</span>) <span class="hljs-string">&quot;Altitude is 1/2 kms.&quot;</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">rocket-descent-to-msg</span> <span class="hljs-string">&quot;done&quot;</span>) <span class="hljs-string">&quot;The rocket has landed!&quot;</span>)<br></code></pre></td></tr></table></figure><p>定义它的桩函数：<code>;(define (rocket-descent-to-msg rd) &quot;&quot;)  ; stub</code>，并且声明将会用<code>RocketDescent</code>的函数模板：<code>;&lt;use template from RocketDescent&gt;</code></p><p>接下来将数据定义部分的函数模板复制下来，将函数名改为<code>rocket-descent-to-msg</code>，并且根据测试来完善函数体。对于剩余距离本身这个数字部分<code>rd</code>，我们需要额外使用<code>(number-&gt;string rd)</code>来将其从<code>Number</code>类型变成<code>String</code>。对于其他字符串部分，可以用<code>string-append</code>拼接：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">rocket-descent-to-msg</span> rd)<br>      (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">and</span></span> (<span class="hljs-name"><span class="hljs-built_in">number?</span></span> rd) (<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> <span class="hljs-number">0</span> rd) (<span class="hljs-name"><span class="hljs-built_in">&lt;=</span></span> rd <span class="hljs-number">100</span>)) (<span class="hljs-name"><span class="hljs-built_in">string-append</span></span> <span class="hljs-string">&quot;Altitude is &quot;</span> (<span class="hljs-name"><span class="hljs-built_in">number-&gt;string</span></span> rd) <span class="hljs-string">&quot; kms.&quot;</span>)]<br>            [<span class="hljs-name"><span class="hljs-built_in">else</span></span> <span class="hljs-string">&quot;The rocket has landed!&quot;</span>]))<br></code></pre></td></tr></table></figure><p>函数设计代码如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; RocketDescent -&gt; String</span><br><span class="hljs-comment">;; produce a broadcast message of rocket&#x27;s descent distance</span><br><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">rocket-descent-to-msg</span> <span class="hljs-number">100</span>) <span class="hljs-string">&quot;Altitude is 100 kms.&quot;</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">rocket-descent-to-msg</span> <span class="hljs-number">0.5</span>) <span class="hljs-string">&quot;Altitude is 1/2 kms.&quot;</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">rocket-descent-to-msg</span> <span class="hljs-string">&quot;done&quot;</span>) <span class="hljs-string">&quot;The rocket has landed!&quot;</span>)<br><br><span class="hljs-comment">;(define (rocket-descent-to-msg rd) &quot;&quot;)  ; stub</span><br><span class="hljs-comment">;&lt;use template from RocketDescent&gt;</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">rocket-descent-to-msg</span> rd)<br>      (<span class="hljs-name"><span class="hljs-built_in">cond</span></span> [(<span class="hljs-name"><span class="hljs-built_in">and</span></span> (<span class="hljs-name"><span class="hljs-built_in">number?</span></span> rd) (<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> <span class="hljs-number">0</span> rd) (<span class="hljs-name"><span class="hljs-built_in">&lt;=</span></span> rd <span class="hljs-number">100</span>)) (<span class="hljs-name"><span class="hljs-built_in">string-append</span></span> <span class="hljs-string">&quot;Altitude is &quot;</span> (<span class="hljs-name"><span class="hljs-built_in">number-&gt;string</span></span> rd) <span class="hljs-string">&quot; kms.&quot;</span>)]<br>            [<span class="hljs-name"><span class="hljs-built_in">else</span></span> <span class="hljs-string">&quot;The rocket has landed!&quot;</span>]))<br></code></pre></td></tr></table></figure></div></details></div>]]>
    </content>
    <id>https://ziling.moe/2025/academics-ubc-cpsc-110-how-to-design-data/</id>
    <link href="https://ziling.moe/2025/academics-ubc-cpsc-110-how-to-design-data/"/>
    <published>2025-07-14T06:40:00.000Z</published>
    <summary>UBC 的计科大一必修课 - CPSC 110</summary>
    <title>UBC - CPSC 110 - How to Design Data</title>
    <updated>2025-07-14T06:40:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term="Academics" scheme="https://ziling.moe/categories/Academics/"/>
    <category term="UBC" scheme="https://ziling.moe/tags/UBC/"/>
    <category term="计算机科学" scheme="https://ziling.moe/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    <category term="教程" scheme="https://ziling.moe/tags/%E6%95%99%E7%A8%8B/"/>
    <content>
      <![CDATA[<p>在上一章，我们体验了 Racket 的语言设计，也简单介绍并使用了<emp>函数</emp> <em>(Function)</em>，但实际上这也是这类语言 —— 类 Lisp/Scheme 语言的精髓之一。</p><p>本章会详细讲解 Racket 语言中的函数，以及<strike>如何将自己的大脑改造成函数</strike>。</p><h2 id="学习目标"><a class="markdownIt-Anchor" href="#学习目标"></a> 学习目标</h2><ul><li>能够运用《如何设计函数》（HtDF）方法设计处理原始数据的函数</li><li>能够阅读完整的函数设计并识别其不同组成部分</li><li>能够评估各元素在清晰度、简洁性及彼此一致性方面的表现</li><li>能够评估整个设计对给定问题的解决效果</li></ul><mark class="tag-plugin colorful mark" color="warning">以下内容涉及到的edX链接均不保证可访问性</mark><h2 id="htdf-recipe"><a class="markdownIt-Anchor" href="#htdf-recipe"></a> HtDF Recipe</h2><p>在本节开始之前，<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/double-starter.rkt">下载来自edX的 double-starter.rkt 文件</a>，将其放在 DrRacket 内：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1111/309;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-functions/double-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-functions/double-starter.webp" alt="double-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">double-starter.rkt</span></div></div><p>HtDF <em>(How to Design Functions)</em> Recipe 告诉我们如何设计出一个好的函数，如何将大事化小，逐个解决。也就是说，在学习函数的开始，我们会将 Recipe 用作学步车，之后熟练运用的时候可以不逐步遵循。</p><ul><li>如果你正处于 edX - How to Code: Simple Data 的课程中，可以访问<a href="https://courses.edx.org/courses/course-v1:UBCx+HtC1x+2T2017/77860a93562d40bda45e452ea064998b/?_gl=1*gd5bbs*_gcl_au*MjAyMTE3ODYwMi4xNzUxMzc5MDk3*_ga*MTIzNjMwNjkzNy4xNzUxMzc5MDk4*_ga_D3KS4KMDT0*czE3NTIxMDcxNjMkbzE0JGcxJHQxNzUyMTA4NjEzJGoxNCRsMCRoMA..#HtDF">edX 内的 HtDF 指南</a></li><li>如果没有，可以访问<a href="https://docs.racket-lang.org/htdf/">Racket 官方的 HtDF 文档</a></li></ul><mark class="tag-plugin colorful mark" color="warning">两个版本的HtDF有显著差异，但我会尽力讲解</mark><p>先让我们看看刚刚的问题：遵循要求设计一个函数，接受一个数字，得到该数字的两倍。</p><p><strong>第一步</strong>，写出它的<emp>签名</emp> <em>(Signiture)</em>，<emp>目的</emp> <em>(Purpose)</em>，和一个<emp>桩</emp> <em>(Stub)</em>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Number -&gt; Number  ; 这是函数的签名，即传入什么类型，得到什么类型</span><br><span class="hljs-comment">;; produce 2 times the given number  ; 这是函数的目的，即它需要做到什么事情</span><br><br>；(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">double</span> n) <span class="hljs-number">0</span>)  <span class="hljs-comment">; 这是个符合函数签名的桩，占位置用</span><br></code></pre></td></tr></table></figure><details class="tag-plugin colorful folding" color="blue" open><summary><p>多个参数的签名</p></summary><div class="body"><p>如果签名的参数涉及多个，请写<code>;; Image Image -&gt; Boolean</code>，用空格，而不要用<code>,</code>一类东西隔开。</p> </div></details><p>桩是指函数的定义，其包含一个可用的函数名、符合要求的参数数量以及一个是正确返回类型的值。</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span>         (<span class="hljs-name">double</span>     n)                <span class="hljs-number">0</span>)<br> ↑ 定义表达式     ↑ 函数名   ↑ 正确的参数数量    ↑ 正确的返回类型的值<br></code></pre></td></tr></table></figure><p><em>ps: 如果你学过其他语言，需要注意桩这个奇怪概念，以及 <code>double</code> 是函数名而非类型</em></p><details class="tag-plugin colorful folding" color="blue" open><summary><p>格式规范</p></summary><div class="body"><p>函数的签名和目的均一行表述完毕，且开头为<code>;; </code>的注释，以便与后面的代码区分。</p> <figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Number -&gt; Number</span><br><span class="hljs-comment">;; produce 2 times the given number</span><br></code></pre></td></tr></table></figure> <p>对于桩，前面只有<code>; </code>，是因为之后会把注释删掉以便使用。</p> <figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs scheme">；(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">double</span> n) <span class="hljs-number">0</span>) <br></code></pre></td></tr></table></figure> <p>即 <code>;; </code>用于表述一些帮助文本，在之后不会被删掉；<code>; </code>是临时注释。</p> </div></details><p>截至目前，我们接触到的基本类型有：<code>Number</code>、<code>String</code>、<code>Image</code>、<code>Boolean</code>。</p><p><strong>第二步</strong>，我们引入<code>check-expect</code>表达式，这个表达式可以传入两个别的表达式，用于判断它们是否相等。相比于我们学过的<code>=</code>运算符来说，它不仅仅是简单的布尔或<code>if</code>判断，更属于<emp>测试</emp> <em>(Testing)</em> 的一部分。</p><p>当我们写出如下代码并运行：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">double</span> n) <span class="hljs-number">0</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">double</span> <span class="hljs-number">3</span>) <span class="hljs-number">6</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">double</span> <span class="hljs-number">4.2</span>) <span class="hljs-number">8.4</span>)<br></code></pre></td></tr></table></figure><p>会有一个窗口出现，告诉你这两个测试（即刚刚我们写的两行<code>check-expect</code>）都失败了。这一步被称为<emp>写样例</emp>。 <em>(Example/Sample)</em></p><p><strong>第三步</strong>，我们开始设计这个函数了，先写出它的<emp>模板</emp> <em>(Template)</em>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">double</span> n)  <span class="hljs-comment">; 这就是模板，虽然和之前的桩一样达不到目的</span><br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> n))  <span class="hljs-comment">; 这只是个占位置的东西，...和n之间需要空格来保证运行通过</span><br></code></pre></td></tr></table></figure><p><strong>第四步</strong>，开始写函数的<emp>函数体</emp> <em>(Body)</em>。就和数学函数一样，输入和结果之间需要给定运算过程，而这个过程就是函数体。开始思考我们该如何让上面写的两个<code>check-expect</code>成立呢？</p><ul><li><code>(check-expect (double 3) 6)</code> 就是 <code>(check-expect (double 3) (* 2 3))</code></li><li><code>(check-expect (double 4.2) 8.4)</code> 就是 <code>(check-expect (double 4.2) (* 2 4.2))</code></li></ul><p>之后，我们知晓这个函数应当让<code>n</code>变成它的两倍，即<code>(* 2 n)</code>，就可以把它作为函数体填进去了。</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">double</span> n)<br>  (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">2</span> n))<br></code></pre></td></tr></table></figure><details class="tag-plugin colorful folding" color="orange" open><summary><p>重复声明</p></summary><div class="body"><p>当定义了一个新的函数或是常量之类，赋予其名时，被称为声明 <em>(Declaration)</em>。在整个程序运行中，我们需要注意这一点，如果你的程序出现了两个相同名字的函数、常量，有可能会出现错误。比如以下代码：</p> <figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">double</span> n) <span class="hljs-number">0</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">double</span> <span class="hljs-number">3</span>) <span class="hljs-number">6</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">double</span> <span class="hljs-number">4.2</span>) <span class="hljs-number">8.4</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">double</span> n)  <span class="hljs-comment">; 错误！double 函数已经定义过了</span><br>  (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">2</span> n))<br></code></pre></td></tr></table></figure> </div></details><p>运行代码之后，交互区出现<code>Both tests passed!</code>，就说明我们通过测试了。</p><hr /><p>总之，写一个函数的步骤大概是：</p><ul><li>定下函数的签名和目的，为此写个桩，给函数占个位置，在测试的时候至少可以<strong>运行</strong></li><li>写几个<code>check-expect</code>表达式用来当这个函数的样例</li><li>完善函数的模板</li><li>写函数体</li><li>通过测试</li></ul><h2 id="simple-practice-when-tests-are-incorrect"><a class="markdownIt-Anchor" href="#simple-practice-when-tests-are-incorrect"></a> Simple Practice &amp; When Tests are Incorrect</h2><p><em>ps: 由于 edX 的这门课需要付费才能查看 Graded Assignments，本人只能根据题目要求来写了</em></p><p>这一节我们会面临第一个 HtDF 问题，即写一个真正有意义的函数：设计一个函数，传入一个单词，得到它的复数形式，假设给任何单词末尾加个<code>s</code>就够了。</p><p>根据上一节提到的步骤，我们可以先<emp>确认函数签名、目的和桩</emp>。签名当然要望文生义一点比较好，而目的也很明确（如题），那开始写桩：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">pluralize</span> word) <span class="hljs-string">&quot;s&quot;</span>)<br></code></pre></td></tr></table></figure><p>之后为这个桩函数写一些<emp>测试</emp>，即<code>check-expect</code>表达式，代入我们预期的结果：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">pluralize</span> <span class="hljs-string">&quot;apple&quot;</span>) <span class="hljs-string">&quot;apples&quot;</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">pluralize</span> <span class="hljs-string">&quot;orange&quot;</span>) <span class="hljs-string">&quot;oranges&quot;</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">pluralize</span> <span class="hljs-string">&quot;banana&quot;</span>) <span class="hljs-string">&quot;bananas&quot;</span>)<br></code></pre></td></tr></table></figure><p>这三个测试肯定是不会通过的，因为我们还没有<emp>实现</emp>，先写出模板，确认我们的函数是围绕<code>word</code>做一些运算的：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">pluralize</span> word)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> word))<br></code></pre></td></tr></table></figure><p>然后就是最重要的部分 —— 实现，我们需要知道怎么给单词后面加<code>s</code>：单词是个<emp>字符串</emp>，<code>&quot;s&quot;</code>当然也是个<emp>字符串</emp>，那么这就是个简单的字符串拼接的问题，所以我们可以使用<code>string-append</code>表达式：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">pluralize</span> word)<br>  (<span class="hljs-name"><span class="hljs-built_in">string-append</span></span> word <span class="hljs-string">&quot;s&quot;</span>))<br></code></pre></td></tr></table></figure><p>最后如果能通过测试，这个函数就完美地写完了。</p><details class="tag-plugin colorful folding" color="blue" open><summary><p>再试试</p></summary><div class="body"><p>实现一个函数：传入一个单词（是问候语，如<code>&quot;hello&quot;</code>, <code>&quot;Bye&quot;</code>一类），得到它后面加了<code>!</code>的单词。（如&quot;<code>&quot;hello!&quot;</code>）</p> </div></details><details class="tag-plugin colorful folding" color="orange" open><summary><p>注意</p></summary><div class="body"><p>我们在编写函数的时候，有时候会出错，无论是语法还是逻辑，最终导致测试不通过。这里我们不仅仅需要思考是不是函数写错了，同时也要确保你的测试本身是没有问题的。比如，在验证一个求正方形面积的函数：</p> <figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">area</span> <span class="hljs-number">3</span>) <span class="hljs-number">3</span>)  <span class="hljs-comment">; ??? 为什么边长为3的正方形面积为3，不是9吗</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">area</span> <span class="hljs-number">3</span>) <span class="hljs-number">9</span>)  <span class="hljs-comment">; 正确的测试  </span><br></code></pre></td></tr></table></figure> </div></details><h2 id="varying-recipe-order"><a class="markdownIt-Anchor" href="#varying-recipe-order"></a> Varying Recipe Order</h2><p>在初学设计函数的时候，困惑是常有的：</p><ul><li>前面的部分看起来既麻烦又啰嗦，但实际上，熟练后的函数设计过程是近乎无感的。</li><li>函数体是一个函数最重要的部分，决定了函数本身的功能。</li><li>不确定函数签名时候，思考这个函数需要什么、得到什么，先把测试样例写出来，再回头写签名。</li></ul><p><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/image-area-starter.rkt">下载来自edX的 image-area-starter.rkt 文件</a>，将其放在 DrRacket 内：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1816/554;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-functions/image-area-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-functions/image-area-starter.webp" alt="image-area-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">image-area-starter.rkt</span></div></div><p>这道题要求我们设计一个函数：接受一个图像并得到图像的面积，也就是说我们需要将这个图像的宽高相乘得到一个值。</p><p>至此，我们已经得到一个<emp>签名</emp>：<code>;; Image -&gt; Number</code>，且函数的<emp>目的</emp>也明确了，即<code>;; produce image's width * height (area)</code>。</p><p>这俩明确后，就可以写<emp>桩函数</emp>了，我们可以为这个函数和参数起一个有意义的名字：<code>(define (image-area img) 0)</code>。</p><p>然后就是函数的<emp>测试</emp>，我们需要为它写个样例：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">image-area</span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)) (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">2</span> <span class="hljs-number">3</span>))<br></code></pre></td></tr></table></figure><p>之后运行测试，显然是不通过的。</p><p><em>ps: 如果遇到了 <code>rectangle: this function is not defined</code>，请在程序开头加上 <code>(require 2htdp/image)</code> 以引入图像库</em></p><p>经过最初的思考，我们需要围绕传入的图像做运算，即这个函数是围绕<code>img</code>在做事情的，所以这是它的<emp>模板</emp>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">image-area</span> img)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> img))<br></code></pre></td></tr></table></figure><p>函数的所有前期构思都结束了，接下来就是最重要的函数体编写，在此之前可以把上面写过的桩函数和模板都注释掉，写一个完整可用的函数：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">image-area</span> img)<br>  (<span class="hljs-name"><span class="hljs-built_in">*</span></span> (<span class="hljs-name">image-width</span> img) (<span class="hljs-name">image-height</span> img)))<br></code></pre></td></tr></table></figure><p>最后，运行测试通过，完整代码如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Image -&gt; Natural</span><br><span class="hljs-comment">;; produce image&#x27;s width * height (area)</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">image-area</span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)) (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">2</span> <span class="hljs-number">3</span>))<br><span class="hljs-comment">; (define (image-area img) 0)  ; stub</span><br><br><span class="hljs-comment">;(define (image-area img)  ; template</span><br><span class="hljs-comment">;  (... img))</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">image-area</span> img)<br>  (<span class="hljs-name"><span class="hljs-built_in">*</span></span> (<span class="hljs-name">image-width</span> img) (<span class="hljs-name">image-height</span> img)))<br></code></pre></td></tr></table></figure><details class="tag-plugin colorful folding" color="blue" open><summary><p>类型</p></summary><div class="body"><p>在本节之前，所有提到的<emp>数字</emp>可能都被<code>Number</code>代指，但到了后期，这一表述将会变得不再准确，比如<emp>小数/浮点数</emp> <em>(Float)</em>。</p> <p><a href="https://docs.racket-lang.org/ts-reference/type-ref.html">有关 Racket 语言有关数字的类型</a></p> <p>Racket 语言的数字类型其实分为很多种，刚刚写的函数签名，更确切地来说，应当是：<code>;; Image -&gt; Natural</code>，即返回值类型是个<emp>自然数</emp>。</p> <p>当然，可以仔细翻阅 Racket 文档，也会发现在 Racket 中，<code>Natural</code>和<code>Exact-Nonnegative-Integer</code>是同义词。</p> </div></details><h2 id="poorly-formed-problems"><a class="markdownIt-Anchor" href="#poorly-formed-problems"></a> Poorly Formed Problems</h2><p>在这一节，我们会遇到表述没有那么清楚的题目。在之后的函数设计中，明确知道函数的需求本身已经将函数设计完成大半了，但真正的难点是需求自身的不清晰。</p><p>先<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/tall-starter.rkt">下载来自edX的 tall-starter.rkt 文件</a>，将其放在 DrRacket 内：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1562/660;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-how-to-design-functions/tall-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-how-to-design-functions/tall-starter.webp" alt="tall-starter.rkt" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">tall-starter.rkt</span></div></div><p>在这道题中，我们需要先确认函数的签名：传入一个图像，得到这个图像高不高。一开始乍一眼可能觉得它的返回值类型是<code>Number</code>一类的，但实际上是个<code>Boolean</code>，即<code>;; Image -&gt; Boolean</code></p><p>同时明确函数的目的，<em>判断图片高不高</em>也是不够细节的，应当是：<code>;; produce true if the image is tall</code>。在明确函数的目的时，需要细节到函数的返回值类型。</p><p>确保引入<code>(require 2htdp/image)</code>，接下来为它写个桩函数和测试样例：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">tall?</span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">2</span> <span class="hljs-number">3</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)) true)<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">tall?</span> img) false)<br></code></pre></td></tr></table></figure><details class="tag-plugin colorful folding" color="blue" open><summary><p>问号结尾的函数名</p></summary><div class="body"><p>如果题目涉及到诸如<strong>判断…</strong>，得到一个<code>Boolean</code>之类的，很有可能需要在函数名末尾加一个<code>?</code>来表示它是需要做出判断并直接给出一个 Yes or No 的答案。</p> </div></details><details class="tag-plugin colorful folding" color="blue" open><summary><p>考虑样例多样性</p></summary><div class="body"><p>在写测试时，有时需要考虑在尽可能多的方面去检测函数的健壮性和安全性。</p> <p>比如这个判断图像高不高的函数，如果传入一个圆或三角形什么的，是否需要特殊处理？</p> </div></details><p>假设这个函数只需要这一个测试样例，完成它的模板：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">tall?</span> img)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> img))<br></code></pre></td></tr></table></figure><p>在实现函数体的时候，我们需要先思考如何判断图像高不高 —— 图像的高大于它的宽 —— 需要一个<code>if</code>表达式判断：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">tall?</span> img)<br>  (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> (<span class="hljs-name">image-height</span> img) (<span class="hljs-name">image-width</span> img))<br>    true<br>    false))<br></code></pre></td></tr></table></figure><p>最后运行，测试通过。</p><details class="tag-plugin colorful folding" color="orange" open><summary><p>测试覆盖率</p></summary><div class="body"><p>取决于你的 DrRacket 个性化设置，你会发现代码运行后，<code>false</code>变成了黑底黄字。</p> <p>这是因为 Racket 发现：运行了程序中所有的<code>check-expect</code>表达式后，有一部分程序始终没有跑过，这部分在 DrRacket 中会被特别标注。</p> <p>测试覆盖率在大型项目中尤为重要，它决定了整个项目的安全、健壮和可维护性。</p> <p>在这套课程中，请务必按照最高覆盖率去写，比如：一个函数需要传2个图像，判断第一个图像是否比第二个图像大，这俩图像需要用它们的宽高，总共就会有<code>3*3=9</code>个<code>check-expect</code></p> </div></details><p>所以我们可以为这个函数再写一个测试，使其可以覆盖到<code>false</code>部分：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">tall?</span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">3</span> <span class="hljs-number">2</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)) false)<br></code></pre></td></tr></table></figure><p>之后，或许还有一个点我们忽略了，由于题目表述模糊，我们并不知道<emp>宽高相等是否意味着图像是高的</emp>，我们应当在写样例的时候确认尽可能多的预期行为和结果 —— 假设这个情况不算高：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">tall?</span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">3</span> <span class="hljs-number">3</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)) false)<br></code></pre></td></tr></table></figure><p>再次运行，测试通过，同时我们也实现了很不错的测试覆盖率。</p><details class="tag-plugin colorful folding" color="blue" open><summary><p>明确所有模糊</p></summary><div class="body"><p>题目当中的模糊请使用注释来明确。</p> </div></details><h2 id="criteria"><a class="markdownIt-Anchor" href="#criteria"></a> Criteria</h2><p>最后，本节会给出函数的评分标准，主要从四个方面：</p><ul><li><strong>Commit Ready / 提交前检查</strong><ul><li>代码简洁</li><li>所有测试代码不应被注释，桩和模板应被注释</li><li>在编辑器里做得草稿最后应被删除</li></ul></li><li><strong>Design Completeness / 设计步骤完整</strong><ul><li>所有 HtDF recipe 提到的步骤应该完整地呈现</li></ul></li><li><strong>Internal Quality / 高质量</strong><ul><li>代码里的每一个设计部分需要即整洁又正确，顺序对应</li><li>函数名望文生义</li><li>所有测试应通过</li><li>测试能做到对程序的全覆盖，不漏分支</li></ul></li><li><strong>Problem Satisfied / 满足题目要求</strong><ul><li>函数设计应遵循题目</li><li>如果题目有部分模糊，请准确识别出并在设计过程中将其确定</li></ul></li></ul><h2 id="practice-problems"><a class="markdownIt-Anchor" href="#practice-problems"></a> Practice Problems</h2><p>这一章的 Recommended Problems:</p><ul><li>HtDF P2 - Less Than 5<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/less-than-five-starter.rkt">less-than-five-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/less-than-five-solution.rkt">less-than-five-solution.rkt</a></li></ul></li><li>HtDF P3 - Boxify<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/boxify-starter.rkt">boxify-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/boxify-solution.rkt">boxify-solution.rkt</a></li></ul></li><li>HtDF P6 - Double Error<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/double-error-starter.rkt">double-error-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/double-error-solution.rkt">double-error-solution.rkt</a></li></ul></li></ul><div class="tag-plugin colorful folders" ><details class="folder" index="0"><summary><p>HtDF P2 - Less Than 5 题解</p></summary><div class="body"><p><strong>预计耗时：10 min / 简单</strong></p><p>这道题需要我们设计一个函数：接受一个字符串，判断它的长度是否小于5，根据 HtDF recipe 作答，需要完整注释。</p><p>按照步骤，我们先思考函数的</p><ul><li>签名：<code>String -&gt; Boolean</code>，这里的坑点是返回值类型应为<code>Boolean</code>。</li><li>目的：<code>produce true if length of s is less than 5</code>，时刻注意需要将目的确切到<emp>要返回什么</emp>上。</li></ul><p>之后再写一些样例，需要覆盖到函数尽可能多的返回可能性，比如：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">less-than-5?</span> <span class="hljs-string">&quot;helloWorld&quot;</span>) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">less-than-5?</span> <span class="hljs-string">&quot;hello&quot;</span>) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">less-than-5?</span> <span class="hljs-string">&quot;hell&quot;</span>) true)<br></code></pre></td></tr></table></figure><p>写一个桩函数：<code>(define (less-than-5? s) true)</code>，再将其变为模板：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">less-than-5?</span> s)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> s))<br></code></pre></td></tr></table></figure><p>最后就可以构思函数体了，由于涉及<emp>字符串的长度</emp>，我们需要<code>string-length</code>表达式；同时又需要<emp>做判断</emp>，故也需要使用<code>&lt;</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">less-than-5?</span> s)<br>  (<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> (<span class="hljs-name"><span class="hljs-built_in">string-length</span></span> s) <span class="hljs-number">5</span>))<br></code></pre></td></tr></table></figure><p>通过测试，完成题目，答案如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; String -&gt; Boolean</span><br><span class="hljs-comment">;; produce true if length of s is less than 5</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">less-than-5?</span> <span class="hljs-string">&quot;&quot;</span>) true)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">less-than-5?</span> <span class="hljs-string">&quot;five&quot;</span>) true)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">less-than-5?</span> <span class="hljs-string">&quot;12345&quot;</span>) false)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">less-than-5?</span> <span class="hljs-string">&quot;eighty&quot;</span>) false)<br><br><span class="hljs-comment">;(define (less-than-5? s)  ;stub</span><br><span class="hljs-comment">;  true)</span><br><br><span class="hljs-comment">;(define (less-than-5? s)  ;template</span><br><span class="hljs-comment">;  (... s))</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">less-than-5?</span> s)<br>  (<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> (<span class="hljs-name"><span class="hljs-built_in">string-length</span></span> s) <span class="hljs-number">5</span>))<br></code></pre></td></tr></table></figure></div></details><details class="folder" index="1"><summary><p>HtDF P3 - Boxify 题解</p></summary><div class="body"><p><strong>预计耗时：15 min / 中等</strong></p><p>这道题让我们设计一个函数：传入一个图像，得到一个<em>被矩形包住</em>的图像。更细节地说：通过创建一个<code>outline rectangle</code>，让它的尺寸刚刚好能够<code>overlay</code>住原图像，<strong>比原图宽高各大2个像素</strong>。</p><p>由题目描述可知函数名定为<code>boxify</code></p><ul><li>签名：<code>Image -&gt; Image</code></li><li>目的：<code>puts a box around given image. Box is 2 pixels wider and taller than given image.</code></li></ul><p><em>ps: 目的描述很难遵循标准答案，此处直接贴上来了</em></p><p>之后为他写样例，这部分在这道题挺难的，算是快把函数体都写出来了：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">check-expect</span> (<span class="hljs-name">boxify</span> (<span class="hljs-name">ellipse</span> <span class="hljs-number">60</span> <span class="hljs-number">30</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)<br>              (<span class="hljs-name">overlay</span> (<span class="hljs-name">ellipse</span> <span class="hljs-number">60</span> <span class="hljs-number">30</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)<br>                       (<span class="hljs-name">rectangle</span> <span class="hljs-number">62</span> <span class="hljs-number">32</span> <span class="hljs-string">&quot;outline&quot;</span> <span class="hljs-string">&quot;black&quot;</span>))))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">boxify</span> (<span class="hljs-name">circle</span> <span class="hljs-number">10</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)) <br>              (<span class="hljs-name">overlay</span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">22</span> <span class="hljs-number">22</span> <span class="hljs-string">&quot;outline&quot;</span> <span class="hljs-string">&quot;black&quot;</span>)<br>                       (<span class="hljs-name">circle</span> <span class="hljs-number">10</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">boxify</span> (<span class="hljs-name">star</span> <span class="hljs-number">40</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;gray&quot;</span>)) <br>              (<span class="hljs-name">overlay</span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">67</span> <span class="hljs-number">64</span> <span class="hljs-string">&quot;outline&quot;</span> <span class="hljs-string">&quot;black&quot;</span>)<br>                       (<span class="hljs-name">star</span> <span class="hljs-number">40</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;gray&quot;</span>)))<br></code></pre></td></tr></table></figure><p>写个桩函数：<code>(define (boxify i) (circle 2 &quot;solid&quot; &quot;green&quot;))</code></p><details class="tag-plugin colorful folding" color="blue" open><summary><p>桩函数的返回值该怎么写</p></summary><div class="body"><p>写任何只要符合返回值类型的值/表达式就行，比如：</p> <ul> <li>如果是<code>Boolean</code>，写<code>true</code>或<code>false</code>都是可以的。</li> <li>如果是<code>Number</code>，随便写个数字都行。</li> <li>如果是<code>Image</code>，捏一个图像就可以。</li> </ul> </div></details><p>然后将其变为模板：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">boxify</span> i) <br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> i))<br></code></pre></td></tr></table></figure><p>从刚刚写的样例就能想到函数体该怎么写了：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">boxify</span> i)<br>  (<span class="hljs-name">overlay</span> (<span class="hljs-name">rectangle</span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name">image-width</span>  i) <span class="hljs-number">2</span>)<br>                      (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name">image-height</span> i) <span class="hljs-number">2</span>)<br>                      <span class="hljs-string">&quot;outline&quot;</span><br>                      <span class="hljs-string">&quot;black&quot;</span>)<br>           i))<br></code></pre></td></tr></table></figure><p>完整代码如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Image -&gt; Image</span><br><span class="hljs-comment">;; Puts a box around given image. Box is 2 pixels wider and taller than given image.</span><br><span class="hljs-comment">;; <span class="hljs-doctag">NOTE:</span> A solution that follows the recipe but makes the box the same width and height </span><br><span class="hljs-comment">;;       is also good. It just doesn&#x27;t look quite as nice. </span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">boxify</span> (<span class="hljs-name">circle</span> <span class="hljs-number">10</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)) <br>              (<span class="hljs-name">overlay</span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">22</span> <span class="hljs-number">22</span> <span class="hljs-string">&quot;outline&quot;</span> <span class="hljs-string">&quot;black&quot;</span>)<br>                       (<span class="hljs-name">circle</span> <span class="hljs-number">10</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">boxify</span> (<span class="hljs-name">star</span> <span class="hljs-number">40</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;gray&quot;</span>)) <br>              (<span class="hljs-name">overlay</span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">67</span> <span class="hljs-number">64</span> <span class="hljs-string">&quot;outline&quot;</span> <span class="hljs-string">&quot;black&quot;</span>)<br>                       (<span class="hljs-name">star</span> <span class="hljs-number">40</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;gray&quot;</span>)))<br><br><span class="hljs-comment">;(define (boxify i) (circle 2 &quot;solid&quot; &quot;green&quot;))</span><br><br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">boxify</span> i)<br>  (<span class="hljs-name"><span class="hljs-built_in">...</span></span> i))<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">boxify</span> i)<br>  (<span class="hljs-name">overlay</span> (<span class="hljs-name">rectangle</span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name">image-width</span>  i) <span class="hljs-number">2</span>)<br>                      (<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name">image-height</span> i) <span class="hljs-number">2</span>)<br>                      <span class="hljs-string">&quot;outline&quot;</span><br>                      <span class="hljs-string">&quot;black&quot;</span>)<br>           i))<br></code></pre></td></tr></table></figure></div></details><details class="folder" index="2"><summary><p>HtDF P6 - Double Error 题解</p></summary><div class="body"><p><strong>预计耗时：7 min / 简单</strong></p><p>这道题是道找 Bug 题，也就是说我们需要找出题目里这部分代码的问题，并以最小幅度将其改对。观察代码：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">;; Number -&gt; Number</span><br><span class="hljs-comment">;; doubles n</span><br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">double</span> <span class="hljs-number">0</span>) <span class="hljs-number">0</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">double</span> <span class="hljs-number">4</span>) <span class="hljs-number">8</span>)<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">double</span> <span class="hljs-number">3.3</span>) (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">2</span> <span class="hljs-number">3.3</span>))<br>(<span class="hljs-name">check-expect</span> (<span class="hljs-name">double</span> <span class="hljs-number">-1</span>) <span class="hljs-number">-2</span>)<br><br><br>#<span class="hljs-comment">;</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">double</span> n) <span class="hljs-number">0</span>) <span class="hljs-comment">; stub</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">double</span> n)<br>  (<span class="hljs-name"><span class="hljs-built_in">*</span></span> (<span class="hljs-name">2</span> n)))<br></code></pre></td></tr></table></figure><p>经过十分细致的逐行检查后，也许会发现最后一行<code>(* (2 n))</code>这里，里面的<code>2 n</code>不应当被套一层括号。</p></div></details></div>]]>
    </content>
    <id>https://ziling.moe/2025/academics-ubc-cpsc-110-how-to-design-functions/</id>
    <link href="https://ziling.moe/2025/academics-ubc-cpsc-110-how-to-design-functions/"/>
    <published>2025-07-10T00:45:00.000Z</published>
    <summary>UBC 的计科大一必修课 - CPSC 110</summary>
    <title>UBC - CPSC 110 - How to Design Functions</title>
    <updated>2025-07-10T00:45:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term="Academics" scheme="https://ziling.moe/categories/Academics/"/>
    <category term="UBC" scheme="https://ziling.moe/tags/UBC/"/>
    <category term="计算机科学" scheme="https://ziling.moe/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    <category term="教程" scheme="https://ziling.moe/tags/%E6%95%99%E7%A8%8B/"/>
    <content>
      <![CDATA[<h2 id="学习目标"><a class="markdownIt-Anchor" href="#学习目标"></a> 学习目标</h2><ul><li>能够编写对基本数据类型（包括数字、字符串、图像和布尔值）进行操作的表达式</li><li>能够编写常量和函数定义</li><li>能够逐步写出简单表达式（包括函数调用）的求值过程</li><li>能够使用步进器自动逐步执行表达式的求值过程</li><li>能够使用 DrRacket Help Desk 来发现新的基本操作</li></ul><mark class="tag-plugin colorful mark" color="warning">以下内容涉及到的edX链接均不保证可访问性</mark><h2 id="expressions"><a class="markdownIt-Anchor" href="#expressions"></a> Expressions</h2><p>首先，在打开 DrRacket 并确保顶部工具栏<code>Language &gt; Choose Language</code>打开后，对话框内选择的是<code>Teaching Languages &gt; Beginning Student</code>，点击<code>OK</code>保存。</p><p>DrRacket 上方编写代码的部分被称为定义区 <em>(Definitions Area)</em>，下方的输出部分则是交互区 <em>(Interaction Area)</em>。</p><p>我们可以在定义区编写一个简单的<emp>表达式</emp>:</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">3</span> <span class="hljs-number">4</span>)<br></code></pre></td></tr></table></figure><p>之后点击右上角的<code>Run</code>按钮，就可以在交互区看到其<emp>值</emp>，即<code>7</code>。</p><p>从这个例子可以看出，Racket 是通过计算<emp>表达式</emp>来得到<emp>值</emp>的。</p><details class="tag-plugin colorful folding" color="blue" open><summary><p>Expressions</p></summary><div class="body"><p>表达式 <em>(Expression)</em> 是程序中被运算 <em>(Evaluate)</em> 以产生值 <em>(Value)</em> 的元素，语法为<code>(&lt;primitive&gt; &lt;expression&gt;)</code>。例如 <code>(+ 3 4) -&gt; 7</code>、<code>(+ 3 (* 2 3)) -&gt; 9</code>、<code>(/ 12 (* 2 3)) -&gt; 2</code></p> <p>上述表达式中的<code>&lt;primitive&gt;</code>有<code>+ * /</code>等，它们被称为基本操作符 <em>(Primitive Operator)</em></p> <p>关键的是，数字本身也是表达式。</p> <p><em>ps: 以防有人不知道，在大多数编程语言中，<code>/</code>是除法</em></p> </div></details><p>之后，我们可以选中目前已经写好的表达式们，点击<code>Racket &gt; Comment Out with &quot;;&quot;</code>，将你选中的表达式<emp>注释</emp>掉。</p><details class="tag-plugin colorful folding" color="blue" open><summary><p>Comment</p></summary><div class="body"><p>在 Racket 中，一行代码前的分号<code>;</code>后的所有内容都是注释 <em>(Comment)</em>，注释旨在向人传达关于程序的重要信息。Racket 在运行时会忽略这些注释。</p> <p><em>ps: 将所有表达式注释掉后，运行将不会有任何值输出</em></p> </div></details><p>在加减乘除之外，本节还会涉及两个基本操作符，第一个是<code>sqr</code>，即平方；以及<code>sqrt</code>，即开平方。后面的表达式可以传递参数 <em>(Argument)</em> 给这两个基本操作符以将其平方/开平方。</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">sqr</span> <span class="hljs-number">3</span>)<br><span class="hljs-comment">; 此处 sqr 是基本操作符。3 是参数，同时也是表达式</span><br>&gt; <span class="hljs-number">9</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">sqrt</span></span> <span class="hljs-number">16</span>)<br><span class="hljs-comment">; 此处 sqrt 是基本操作符。16 是参数，同时也是表达式</span><br>&gt; <span class="hljs-number">4</span><br></code></pre></td></tr></table></figure><p><em>之后会学到 Parameter 的概念，极容易与 Argument 混淆，这里打个预防针</em></p><p>接下来是课后小练习，我们可以先<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/pythag-starter.rkt">下载来自edX的 pythag-starter.rkt 文件</a>。将这个文件拖动到 DrRacket 中，或者在上方工具栏<code>File &gt; Open...</code>中选择该文件打开。之后就能看到一个<em>带有题目</em>的代码文件。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1111/545;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-beginning-student-language/pythag-starter.webp" data-src="/images/2025/academics-ubc-cpsc-110-beginning-student-language/pythag-starter.webp" alt="题目" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">题目</span></div></div><p>你可以选中你想要将其变成注释一部分的表达式们，然后点击<code>Racket &gt; Comment Out with a Box</code>，将它们变成注释块。</p><details class="tag-plugin colorful folding" color="green"><summary><p>Solution</p></summary><div class="body"><p>已知直角三角形的两条直角边长，求斜边长度，可用勾股定理解决。</p> <p><code>(sqrt (+ (sqr 3) (sqr 4))) -&gt; 5</code></p> </div></details><p>之后我们可以尝试一下运行<code>(sqrt 2)</code>这个表达式，结果的前面会有<code>#i</code>。这是因为2的平方根是个无理数，Racket 语言会通过附加<code>#i</code>来表示这是一个不精确 <em>(Inexact)</em> 的结果。</p><h2 id="evaluation"><a class="markdownIt-Anchor" href="#evaluation"></a> Evaluation</h2><p>上一节提到的都是很简单的表达式，但当我们遇到很复杂的程序代码时，我们需要尝试去理解、分析并计算它们。</p><p>对于一个表达式<code>(+ 2 (* 3 4) (- (+ 1 2) 3))</code>，我们知道它的值是<code>14</code>。</p><p>分析该表达式可以得到它是由以下元素组成的：</p><ul><li><code>+</code>: 加法运算符 <em>(Operator)</em></li><li><code>2</code>, <code>(* 3 4)</code>, <code>(- (+ 1 2) 3)</code>: 都是参与运算的操作数 <em>(Operand)</em></li><li><code>(+ 2 (* 3 4) (- (+ 1 2) 3))</code>: 整个表达式是一次对基本操作 <em>(Primitive)</em> 的调用 <em>(Call)</em></li></ul><p><em>ps: 操作数里有两个还能深入分析的表达式，可以同理得到它们的基本操作符、操作数和调用</em></p><details class="tag-plugin colorful folding" color="blue" open><summary><p>Primitive Call</p></summary><div class="body"><p>Primitive 在这里代指的是诸如<code>+ - * /</code>一类的基本操作，而 Call，即调用则意味着该操作被执行了，所以这个词组在这里更像是描述一个过程。</p> <p>在计算一个 Primitive Call 时，我们需要随着括号将操作数都算出来，整体运算顺序为从左至右，从里至外，具体运算顺序如下:</p> <ol> <li><code>(+ 2 (* 3 4) (- (+ 1 2) 3))</code></li> <li><code>(+ 2 12      (- (+ 1 2) 3))</code></li> <li><code>(+ 2 12      (- 3       3))</code></li> <li><code>(+ 2 12      0)</code></li> <li><code>14</code></li> </ol> </div></details><h2 id="strings-and-images"><a class="markdownIt-Anchor" href="#strings-and-images"></a> Strings and Images</h2><p>这一节会带来两类新的值 <em>(Primitive Value)</em>，即<emp>字符串</emp> <em>(String)</em> 和<emp>图像</emp> <em>(Image)</em>，同时也会围绕这两类值进行一些操作。</p><p>字符串形如生活中的句子、单词等，在描述一串字符的时候，我们通常用<code>&quot;some words&quot;</code>来表示。出现在字符串两端的应当是双引号，而其内部则是真正要表达的一串字符。</p><p>可以尝试在 DrRacket 中执行一行<code>&quot;Apple&quot;</code>，它的输出结果也仅仅是一行<code>&quot;Apple&quot;</code>。</p><p>我们可以围绕字符串做一些操作，比如<code>string-append</code>，这个操作可以将所有字符串拼接在一起。</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">string-append</span></span> <span class="hljs-string">&quot;Hello&quot;</span> <span class="hljs-string">&quot;World&quot;</span>)<br>&gt; <span class="hljs-string">&quot;HelloWorld&quot;</span><br>(<span class="hljs-name"><span class="hljs-built_in">string-append</span></span> <span class="hljs-string">&quot;Hello&quot;</span> <span class="hljs-string">&quot; &quot;</span> <span class="hljs-string">&quot;World&quot;</span>)  <span class="hljs-comment">; 多少参数都是可以用的</span><br>&gt; <span class="hljs-string">&quot;Hello World&quot;</span><br></code></pre></td></tr></table></figure><p>从字符串的定义要求我们知道，它需要两个双引号来包裹自己，所以可以以此清晰的发现<code>123</code>和<code>&quot;123&quot;</code>的区别（一个是数字，一个是字符串）。</p><p>数字的一些操作，比如<code>(+ 1 123)</code>，我们知道其值是<code>124</code>。但当你尝试<code>(+ 1 &quot;123&quot;)</code>的时候，会发现下方的交互区出现了一个<emp>异常/错误</emp> <em>(Exception)</em>，这类异常十分常见于将数字和字符串弄混的时候。</p><blockquote><p>+: expects a number, given “123”</p></blockquote><p>第二个有关字符串的操作就是获取其长度，即<code>string-length</code>，可以试一下诸如<code>(string-length &quot;hello&quot;)</code>看看其值是什么。</p><p>第三个操作是<code>substring</code>，它的意思是从字符串中<emp>截取</emp>一段字符串，我们看看以下例子：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">substring</span></span> <span class="hljs-string">&quot;Caribou&quot;</span> <span class="hljs-number">2</span>             <span class="hljs-number">4</span>)<br><span class="hljs-comment">;          ↑ 字符串   ↑ 起始位置    ↑截止位置(不包含)</span><br>&gt; <span class="hljs-string">&quot;ri&quot;</span><br></code></pre></td></tr></table></figure><p>如果是第一次接触编程，可能会疑惑其值为什么不是<code>&quot;ar&quot;</code>，这是因为在绝大多数的编程语言中，<emp>索引/位置</emp> <em>(Index)</em> 都是从<emp>0</emp>开始算的，所以在这个字符串中，所有字符的位置如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-string">&quot;Caribou&quot;</span><br><span class="hljs-comment">;0123456</span><br></code></pre></td></tr></table></figure><hr /><p>以上就是字符串的相关内容，Racket 也对作图做了支持，本节会涉及一种。</p><p>为了能方便地操作多个代码文件，我们可以在 DrRacket顶部工具栏中的<code>File &gt; New Tab</code>来创建新标签页。</p><p>在编辑区中，先输入<code>(require 2htdp/image)</code>，这一行意为从<code>2htdp</code>这个地方<emp>引入</emp><code>image</code>相关的<emp>函数</emp>。 <em>(近似于 Operator)</em></p><p>然后我们在第二行输入<code>(circle 10 &quot;solid&quot; &quot;red&quot;)</code>，点击运行，就可以在交互区发现一个红色的实心圆，以此可以得知这一行的意思是创建一个半径为10的红色实心圆。</p><p>还可以输入<code>(rectangle 30 60 &quot;outline&quot; &quot;blue&quot;)</code>，得到一个高60宽30的蓝色空心矩形。</p><p>除了图形之外，也可以渲染文字作为图形：<code>(text &quot;hello&quot; 24 &quot;orange&quot;)</code>，得到一个字号为24的橙色Hello。</p><p>接下来我们围绕图形做一些操作，尝试如下代码：</p><div class="tag-plugin grid"  style="grid-template-columns: repeat(2, 1fr);"><div class="cell" style="">    <figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">above</span> (<span class="hljs-name">circle</span> <span class="hljs-number">10</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)<br>       (<span class="hljs-name">circle</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;yellow&quot;</span>)<br>       (<span class="hljs-name">circle</span> <span class="hljs-number">30</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;green&quot;</span>))<br></code></pre></td></tr></table></figure>    </div>    <div class="cell" style="">    <div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:85/156;height:100px;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-beginning-student-language/images-above.webp" data-src="/images/2025/academics-ubc-cpsc-110-beginning-student-language/images-above.webp" alt="垂直堆叠" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">垂直堆叠</span></div></div>    </div>    </div><p>运行后，就能在交互区发现三个不同颜色、不同大小的圆<emp>垂直堆叠</emp>在了一起。那么如何将它<emp>水平排列</emp>？</p><div class="tag-plugin grid"  style="grid-template-columns: repeat(2, 1fr);"><div class="cell" style="">    <figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">beside</span> (<span class="hljs-name">circle</span> <span class="hljs-number">10</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)<br>        (<span class="hljs-name">circle</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;yellow&quot;</span>)<br>        (<span class="hljs-name">circle</span> <span class="hljs-number">30</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;green&quot;</span>))<br></code></pre></td></tr></table></figure>    </div>    <div class="cell" style="">    <div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:157/84;height:100px;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-beginning-student-language/images-beside.webp" data-src="/images/2025/academics-ubc-cpsc-110-beginning-student-language/images-beside.webp" alt="水平排列" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">水平排列</span></div></div>    </div>    </div><p>或是将它们<emp>堆在一起</emp>，逐层覆盖：</p><div class="tag-plugin grid"  style="grid-template-columns: repeat(2, 1fr);"><div class="cell" style="">    <figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">overlay</span> (<span class="hljs-name">circle</span> <span class="hljs-number">10</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)<br>         (<span class="hljs-name">circle</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;yellow&quot;</span>)<br>         (<span class="hljs-name">circle</span> <span class="hljs-number">30</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;green&quot;</span>))<br></code></pre></td></tr></table></figure>    </div>    <div class="cell" style="">    <div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:85/85;height:100px;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-beginning-student-language/images-overlay.webp" data-src="/images/2025/academics-ubc-cpsc-110-beginning-student-language/images-overlay.webp" alt="堆在一起，逐层覆盖" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">堆在一起，逐层覆盖</span></div></div>    </div>    </div><h2 id="constant-definitions"><a class="markdownIt-Anchor" href="#constant-definitions"></a> Constant Definitions</h2><p>在这一节，我们会涉及到<emp>常量</emp> <em>(Constant)</em> 的概念。它与之前我们接触到的数字、字符串等很像，但它的特性是：自它被定义之初，之后就不会改变。</p><p>或许在其他编程语言的教程中，初学者会先接触到变量的概念，然后再与常量对比。但在这里，我们会先接触常量，一个或许更接近现实生活的概念。</p><p><em>ps: 其他语言中的常量很可能和这里不太一样，这里的常量更像是 只读的变量</em></p><p>比如<code>Π</code> <em>(≈3.1415)</em> 这个众所周知的常量，它自定义以来就不会变。在程序中也是一样，我们也可以定义一个不变量，并赋予一个名字。</p><p>接上一节绘图部分，我们可以定义如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">require</span></span> <span class="hljs-number">2</span>htdp/image)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> WIDTH <span class="hljs-number">400</span>)   <span class="hljs-comment">; 注意：常量的名字应当是大写的，当然它可以由很多字符组成</span><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> HEIGHT <span class="hljs-number">600</span>)  <span class="hljs-comment">; 常量之后跟的是它的值</span><br></code></pre></td></tr></table></figure><p>正如我们在写数学公式时，涉及有关圆、球一类的运算，可能会将<code>Π</code>写上一样，我们也可以在代码中用它们的<emp>名字</emp>来体现它的值：<code>(* WIDTH HEIGHT) -&gt; 240000</code></p><p>常量定义的语法是：<code>(define &lt;name&gt; &lt;expression&gt;)</code></p><p>或许会发现一些有意思的地方，由于定义常量时最后的参数是<code>&lt;expression&gt;</code>，我们在定义时并不一定需要填一个确定的数、字符串进去，诸如<code>1/2</code>一类的表达式也是可以的。</p><div class="tag-plugin quot p"><p class="content"><img class="icon prefix" src="https://bu.dusays.com/2022/10/24/63567d3e092ff.png" /><span class="text">所有的值都是表达式。</span><img class="icon prefix" src="https://bu.dusays.com/2022/10/24/63567d3e0ab55.png" /></p></div><div class="tag-plugin grid"  style="grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));"><div class="cell" style="">    <p>我们可以从 <a href="https://htdp.org/2018-01-06/Book/part_one.html">HtDP</a> 找到一只猫，如图：</p><p>之后将这只猫的图片复制粘贴到 DrRacket 中<code>(define CAT &lt;图片粘贴处&gt;)</code>这里。</p><p><em>ps: 这编辑器能塞图片属实惊到我了</em></p>    </div>    <div class="cell" style="">    <div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:75/117;height:100px;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-beginning-student-language/constant-cat.webp" data-src="/images/2025/academics-ubc-cpsc-110-beginning-student-language/constant-cat.webp" alt="小猫" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">小猫</span></div></div>    </div>    </div><p>之后我们就可以对这只猫进行一些绘图相关的操作了，比如让它旋转<code>-10°</code>: <code>(rotate -10 CAT)</code>，点击运行后，就可以在交互区看到一只斜着的猫。</p><p>再让它旋转<code>10°</code>: <code>(rotate 10 CAT)</code>，就能再看到另一只斜着的猫。</p><p>我们也可以各自赋予他们一个名字，比如向右倾斜的猫是<code>RCAT</code>，向左倾斜的猫是<code>LCAT</code>:</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> RCAT (<span class="hljs-name">rotate</span> <span class="hljs-number">-10</span> CAT))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> LCAT (<span class="hljs-name">rotate</span> <span class="hljs-number">10</span> CAT))<br></code></pre></td></tr></table></figure><p>这两只猫就被存在了两个常量中，它们不再是直接的<emp>值</emp> <em>(图片)</em>，而是被<emp>名字</emp>代指的<emp>表达式/值</emp>。</p><p>这一节非常非常重要，不亚于小时候第一次学到 <em>用字母表示数</em> 对数学思想的改变 —— <emp>用名字表示表达式</emp>。</p><p>很多时候我们编写的代码并不能像前几节一样一句推出结果，而是好几步。中间的运算结果需要我们通过<code>define</code>来存储并使用。</p><h2 id="function-definitions"><a class="markdownIt-Anchor" href="#function-definitions"></a> Function Definitions</h2><p>这一节会引入一个新的概念 —— <emp>函数</emp>。这个概念在数学学习中早已耳熟能详，在计算机里会发现其功能和数学当中的函数很像。</p><p>再开始之前，先下载<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/function-definitions-starter.rkt">edX 的 function-definitions-starter.rkt 文件</a>，并在 DrRacket 中打开。代码如下：</p><figure class="highlight scheme"><figcaption><span>function-definitions-starter.rkt</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">require</span></span> <span class="hljs-number">2</span>htdp/image)<br><br><span class="hljs-comment">;; function-definitions-starter.rkt</span><br><br>(<span class="hljs-name">above</span> (<span class="hljs-name">circle</span> <span class="hljs-number">40</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)         <br>       (<span class="hljs-name">circle</span> <span class="hljs-number">40</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;yellow&quot;</span>)<br>       (<span class="hljs-name">circle</span> <span class="hljs-number">40</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;green&quot;</span>))<br></code></pre></td></tr></table></figure><p>该代码文件执行后，应当是形同红绿灯一样的三个圆出现在交互区。</p><p>我们观察三行绘图代码，会发现有一些地方是重复出现的，而只有最后的字符串是会变的。这里就能用到函数的第一个功能，<emp>削减代码冗余</emp> <em>(Redundancy)</em>。</p><p>该如何做到这一点呢？回顾曾经学过数学意义上的函数：<code>f(x) = 2*x</code>，如果<code>x=2</code>，结果是<code>4</code>；如果<code>x=6</code>，结果就是<code>12</code>。</p><p>编程里的函数和这一过程很像：</p><ul><li>可以重复使用</li><li>可以更改传入的值以得到不同的结果</li></ul><p>在 Racket 语言中，函数定义语法如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">&lt;function-name&gt;</span> &lt;parameter&gt; &lt;body&gt;))<br></code></pre></td></tr></table></figure><p>我们在代码后添加如下函数：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">bulb</span> c)  <span class="hljs-comment">; bulb 是函数名，c 是参数</span><br>  (<span class="hljs-name">circle</span> <span class="hljs-number">40</span> <span class="hljs-string">&quot;solid&quot;</span> c))  <span class="hljs-comment">; 函数体</span><br></code></pre></td></tr></table></figure><p>在这个函数里，<code>c</code>就代表着自变量。之后我们就可以再写一行<code>(bulb &quot;purple&quot;)</code>来<emp>调用</emp>该函数，运行后就会多出一个紫色的圆。</p><p>之后，一开始的那三行代码都可以被这个函数简化掉，变成如下最终效果相同的代码：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">above</span> (<span class="hljs-name">bulb</span> <span class="hljs-string">&quot;red&quot;</span>) (<span class="hljs-name">bulb</span> <span class="hljs-string">&quot;yellow&quot;</span>) (<span class="hljs-name">bulb</span> <span class="hljs-string">&quot;green&quot;</span>))<br></code></pre></td></tr></table></figure><p>以后使用函数就和用其他学过的基本操作符 (如<code>string-append</code>) 一样了。举个例子，<code>string-append</code> 的作用是拼接字符串，如<code>(string-append &quot;re&quot; &quot;d&quot;)</code>的值是字符串<code>&quot;red&quot;</code>。我们就可以将<code>(bulb &quot;red&quot;)</code>变成等效的<code>(string-append &quot;re&quot; &quot;d&quot;)</code>，层层嵌套。</p><h2 id="booleans-and-if-expressions"><a class="markdownIt-Anchor" href="#booleans-and-if-expressions"></a> Booleans and if Expressions</h2><p>生活中有许多答案是对或错的问题，回答它们或许很简单，但也很具有决定性。对于 Racket 和其他编程语言也一样，对或错的答案会影响程序后续运行的走向。</p><p>这一节会引入<emp>布尔</emp> <em>(Boolean)</em> 的概念，如果你是第一次听到这个词的话，现在只需要知道它和数字、字符串等概念并列（它也是个类型）；但它与其它类型的不同在于，它<emp>只有两个可能的值</emp>：真 <em>(True)</em> 与 假 <em>(False)</em>。</p><p>在 Racket 中，这两个布尔值的表述为<code>true</code>和<code>false</code>，<emp>注意小写</emp>。和数字、字符串等一样，可以直接在 DrRacket 中运算。在编辑器中单输入两行<code>true</code>和<code>false</code>，交互区就会出现一行<code>true</code>和一行<code>false</code>。</p><p>没有问题只有答案十分无趣。让我们在编辑器中定义<code>WIDTH</code>为<code>100</code>、<code>HEIGHT</code>为<code>100</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> WIDTH <span class="hljs-number">100</span>)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> HEIGHT <span class="hljs-number">100</span>)<br></code></pre></td></tr></table></figure><p>接下来提问：<code>WIDTH</code>比<code>HEIGHT</code>大吗？</p><p>如何让 Racket 回答问题呢？在数学中我们知道比较两个数的大小可以用包括但不限于<code>&gt; &lt; =</code>等符号，在 Racket 中也一样：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> WIDTH HEIGHT)  <span class="hljs-comment">; 使用大于号判断</span><br></code></pre></td></tr></table></figure><p>当然，也可以试试其他的基本操作符，比如<code>(= WIDTH HEIGHT)</code>和<code>(&gt;= WIDTH HEIGHT)</code>之类的值都是<code>true</code>。</p><p>让这种基本操作符或者函数运算得到<code>true</code>或<code>false</code>的行为被称为<emp>断言</emp>。 <em>(Predicate/Assert)</em></p><p>字符串的逻辑运算也可以使用，比如<code>(string=? &quot;foo&quot; &quot;bar&quot;) -&gt; false</code>，<code>string=?</code>可以比较两个字符串是否相同。</p><p>同样的，图像的一些属性也可以拿来比较，比如两个图像的宽度（使用<code>image-width</code>，故<code>image-height</code>同理）：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">require</span></span> <span class="hljs-number">2</span>htdp/image)<br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> I1 (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> I2 (<span class="hljs-name">rectangle</span> <span class="hljs-number">20</span> <span class="hljs-number">10</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>))<br><br>(<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> (<span class="hljs-name">image-width</span> I1) (<span class="hljs-name">image-width</span> I2))<br></code></pre></td></tr></table></figure><hr /><p>布尔促成了程序<emp>控制流</emp> <em>(Control Flow)</em> 的存在，我们可以借助布尔让程序的运行结果不再一成不变。在 Racket 中，可以借助<code>if</code>表达式让程序运行出现分支，其语法如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">if</span></span> &lt;expression&gt;  <span class="hljs-comment">; 一个值为布尔的条件</span><br>    &lt;expression&gt;  <span class="hljs-comment">; 如果布尔值为 true，执行这个表达式</span><br>    &lt;expression&gt;  <span class="hljs-comment">; 如果布尔值为 false，执行这个表达式</span><br>    )<br></code></pre></td></tr></table></figure><p>保留之前的代码，让我们来尝试一下<code>if</code>表达式：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">require</span></span> <span class="hljs-number">2</span>htdp/image)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> I1 (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> I2 (<span class="hljs-name">rectangle</span> <span class="hljs-number">20</span> <span class="hljs-number">10</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>))<br><br>(<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> (<span class="hljs-name">image-width</span> I1) (<span class="hljs-name">image-width</span> I2))  <span class="hljs-comment">; 该表达式的值是布尔类型的</span><br>    <span class="hljs-string">&quot;tall&quot;</span>  <span class="hljs-comment">; 如果 I1 的宽度小于 I2 的，那么输出 tall</span><br>    <span class="hljs-string">&quot;wide&quot;</span>  <span class="hljs-comment">; 否则输出 wide</span><br>    )<br></code></pre></td></tr></table></figure><p><em>ps: 如果第一个表达式，即条件，值不是布尔类型的，会报错，可以试试</em></p><p>当然，<code>(if true &quot;true&quot; &quot;false&quot;) -&gt; true</code>，<code>(if false &quot;true&quot; &quot;false&quot;) -&gt; false</code>。</p><hr /><p>本节的最后一个概念是<emp>逻辑运算符</emp>，即<emp>与</emp> <em>(And)</em>、<emp>或</emp> <em>(Or)</em>、<emp>非</emp> <em>(Not)</em> 等等，它们很重要，因为单纯一个逻辑运算符在很多时候难以表达我们的意思，比如：</p><ul><li>当你的银行账户密码输入正确 <emp>且</emp> 你的账户内资金足额，那么你就可以取钱。</li><li>如果你考到托福 110 分 <emp>或</emp> 你在英语国家生活4年以上，那么你就可以免除英语要求</li><li>世上有两种人，一类是懂二进制的人，一类是 <emp>不懂</emp> 二进制的人。</li></ul><p>回顾刚刚我们留下的两行图像定义，如果我们需要同时比较<code>I1</code>和<code>I2</code>的高与<code>I1</code>和<code>I2</code>的宽：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> (<span class="hljs-name">image-height</span> I1) (<span class="hljs-name">image-height</span> I2))<br>(<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> (<span class="hljs-name">image-width</span> I1) (<span class="hljs-name">image-width</span> I2))<br></code></pre></td></tr></table></figure><p>将它们用<code>and</code>的逻辑关系运算，得到最终的值，我们可以写：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">and</span></span> (<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> (<span class="hljs-name">image-height</span> I1) (<span class="hljs-name">image-height</span> I2))<br>     (<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> (<span class="hljs-name">image-width</span> I1) (<span class="hljs-name">image-width</span> I2)))<br></code></pre></td></tr></table></figure><p><code>and</code>接受两个表达式，且两个表达式的值只能是布尔类型，其运算过程如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs scheme"><span class="hljs-comment">; Step 1</span><br>(<span class="hljs-name"><span class="hljs-built_in">and</span></span> (<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> (<span class="hljs-name">image-height</span> I1) (<span class="hljs-name">image-height</span> I2))<br>     (<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> (<span class="hljs-name">image-width</span> I1) (<span class="hljs-name">image-width</span> I2)))<br><span class="hljs-comment">; Step 2</span><br>(<span class="hljs-name"><span class="hljs-built_in">and</span></span> true<br>     (<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> (<span class="hljs-name">image-width</span> I1) (<span class="hljs-name">image-width</span> I2)))<br><span class="hljs-comment">; Step 3</span><br>(<span class="hljs-name"><span class="hljs-built_in">and</span></span> true true)<br><span class="hljs-comment">; Step 4</span><br>true<br></code></pre></td></tr></table></figure><details class="tag-plugin colorful folding" color="orange" open><summary><p>短路机制</p></summary><div class="body"><p>观察代码的第二、三步，你会发现<code>and</code>似乎没有同时将两个表达式运算出来，而是有先后的。</p> <p>这个现象在各编程语言都存在 —— 考虑到性能问题，它们会先看前面的表达式的值是不是<code>false</code>，<emp>如果是的话就不需要运算后面的表达式了</emp>，因为对于<code>and</code>来说，它需要两个表达式同时为<code>true</code>，所以只要有一个不是<code>true</code>，<code>and</code>运算就可以立即终止了。</p> <p>这一现象在<code>or</code>和<code>not</code>这类需要将所有表达式都得运算一遍的逻辑运算符身上不存在。</p> </div></details><h2 id="using-the-stepper"><a class="markdownIt-Anchor" href="#using-the-stepper"></a> Using the Stepper</h2><p>在介绍<emp>步进器</emp> <em>(Stepper)</em> 之前，先<a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/stepper-starter.rkt">下载来自edX的 stepper-starter.rkt 文件</a>，即：</p><figure class="highlight scheme"><figcaption><span>stepper-starter.rkt</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">require</span></span> <span class="hljs-number">2</span>htdp/image)<br><br><span class="hljs-comment">;; stepper-starter.rkt</span><br><br>(<span class="hljs-name"><span class="hljs-built_in">+</span></span> (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">3</span> <span class="hljs-number">2</span>) <span class="hljs-number">1</span>)<br><br><br><br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">max-dim</span> img)<br>  (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> (<span class="hljs-name">image-width</span> img) (<span class="hljs-name">image-height</span> img))<br>      (<span class="hljs-name">image-width</span> img)<br>      (<span class="hljs-name">image-height</span> img)))<br><br><br>(<span class="hljs-name">max-dim</span> (<span class="hljs-name">rectangle</span> <span class="hljs-number">10</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>))<br></code></pre></td></tr></table></figure><p>这一大段代码看着很困惑，可以先运行下看看会输出什么：<code>7</code>与<code>20</code>。</p><p>大多数时候，我们可以逐句分析代码的过程并在脑海中构思，但对于一些复杂的情况，我们需要借助 Racket 自己的工具来帮我们还原代码执行过程。</p><p>我们点开<code>Run</code>按钮左边的<code>Step</code>，会有一个新窗口弹出：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:1009/478;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-beginning-student-language/stepper.webp" data-src="/images/2025/academics-ubc-cpsc-110-beginning-student-language/stepper.webp" alt="Stepper 窗口 description" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">Stepper 窗口 description</span></div></div><p>在窗口的左边是<emp>Racket 正在运算的东西</emp>，右边则是<emp>Racket 对这一步的运算结果</emp>。</p><p>比如第一步，左边的绿色部分会运算成右边的紫色部分。点击上方的<code>Next</code>就可以让 Racket 执行下一步运算，显然，这一行表达式的最终值是<code>7</code>。</p><p>我们直接到了最后一行，即<code>(max-dim (rectangle 10 20 &quot;solid&quot; &quot;blue&quot;))</code>，里面的<code>rectangle</code>在右边会被运算成一个小蓝色矩形。</p><p>再下一步，神奇的事情出现了，上面对<code>max-dim</code>的定义内容全部都复制到了刚刚的蓝色矩形上 —— <code>max-dim</code>的<code>img</code>都被替换成了蓝色矩形，参与运算。</p><p>之后，Racket 开始处理<code>if</code>表达式的<emp>条件</emp>，这一行的<code>image-width</code>和<code>image-height</code>变成了<code>10</code>和<code>20</code>。然后<code>(&gt; 10 20)</code>的值显然是<code>false</code>，最后就走向了第二个表达式 —— 算蓝色矩形的高度，值为<code>20</code>，并输出到交互区。</p><p>程序结束了，如果对于上面的步骤不太清楚，可以点击<code>Previous</code>来返回上一步。</p><p>Stepper 让我们能更清晰的知道 Racket 是如何一步步处理我们的程序的，这在学习该语言中帮助很大。</p><p><em>ps: 在其他语言当中，类似的功能是 <code>Debugger</code>，即调试器</em></p><h2 id="discovering-primitives"><a class="markdownIt-Anchor" href="#discovering-primitives"></a> Discovering Primitives</h2><p>在前面的小节中，许多基本操作符是渐进地出现的：</p><ul><li>在学习数字时，同时认识到<code>+ - * /</code></li><li>在学习字符串时，我们知道用<code>string-append</code>等来操作它们</li><li>在学习图像时，<code>image-width</code>和<code>image-height</code>可以获取它们的宽高</li><li>在学习布尔运算时，我们接触了<code>&lt; &gt; =</code>和<code>and or not</code>等</li></ul><p>但教学内容是有限的，Racket 语言自带的基本操作符还有很多很多，我们该如何接触那些从未认识过的基本操作符呢？</p><p>其中一个方法是：<emp>猜</emp>。很多时候，编程语言的标准是一致的，可以基于它的<emp>命名规范</emp>来猜测，比如在之前的图像一节中，我们学过<code>rectangle</code>和<code>circle</code>来绘制，比如：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">require</span></span> <span class="hljs-number">2</span>htdp/image)<br><br>(<span class="hljs-name">overlay</span> (<span class="hljs-name">circle</span> <span class="hljs-number">10</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;red&quot;</span>)<br>         (<span class="hljs-name">circle</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;yellow&quot;</span>)<br>         (<span class="hljs-name">circle</span> <span class="hljs-number">30</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;green&quot;</span>))<br></code></pre></td></tr></table></figure><p>那么我们就可以合理猜测：<code>triangle</code>是否存在？假设它存在，那么对于一个三角形来说，它也许需要尺寸、是否实心、颜色什么的：<code>(triangle 40 &quot;solid&quot; &quot;purple&quot;)</code></p><p>将这一行代码放进编辑器中运行发现，交互区竟然真的出现一个紫色三角形。但疑惑的事情出现了，<emp>什么是三角形的尺寸？</emp></p><p>接下来让我们把鼠标指针放在<code>triangle</code>上：</p><ul><li>如果你是 Windows 系统，请右键</li><li>如果你是 MacOS 系统，请摁住<kbd>Ctrl</kbd>的同时按下鼠标</li></ul><p>在弹出的框中，点击<code>Search in Help Desk for &quot;triangle&quot;</code>，之后会出现一个网页。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:920/520;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-beginning-student-language/help-desk.webp" data-src="/images/2025/academics-ubc-cpsc-110-beginning-student-language/help-desk.webp" alt="Racket 官方文档查阅" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">Racket 官方文档查阅</span></div></div><p>从之前的图像绘制中我们知道这一切都是因为我们导入了<code>2htdp/image</code>这个库，让我们点击上图中的第一行：<code>triangle provided from 2htdp/image</code>，进入到详情页：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:935/517;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-beginning-student-language/help-desk-triangle.webp" data-src="/images/2025/academics-ubc-cpsc-110-beginning-student-language/help-desk-triangle.webp" alt="Racket 官方文档 - Polygons - Triangle" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">Racket 官方文档 - Polygons - Triangle</span></div></div><p>我们观察文档上描述<code>triangle</code>的语法，会发现第一个参数的意思是<code>side-length</code>，即边长。这下破案了，我们刚刚填的<code>40</code>实际上是指三角形的边长。</p><hr /><p>接下来是第二种探究方法，我们先开个新标签页，填入：<code>(/ 3 4)</code>，根据之前的学习，当然知道它的值是<code>0.75</code>。</p><p>在一些场景中，我们需要知道一次除法运算结果<emp>四舍五入</emp> <em>(Round)</em> 后的值，该去哪找呢？</p><p>我们再次以同样方式，点击<code>/</code>的<code>Search in Help Desk for &quot;/&quot;</code>，选中网页内的<code>/ provided from lang/htdp-beginner</code>。</p><p>里面有大量的基本操作符，我们需要极具耐心地在这一页寻找四舍五入的写法。可以借助浏览器的全局搜索（一般来说可以按下 <kbd>Ctrl + F</kbd> ），然后输入<code>round</code>。之后我们就会被定位到<code>round</code>部分的所在位置。</p><p>四舍五入的写法：<code>(round &lt;real number&gt;)</code>，可以直接复制下来到 DrRacket 尝试，<code>(round (/ 3 4)) -&gt; 1</code>。</p><div class="tag-plugin blockquote" indent="undefined"><p>借助文档是学习并深入了解一门编程语言几乎最重要的方式。</p></div><h2 id="practice-problems"><a class="markdownIt-Anchor" href="#practice-problems"></a> Practice Problems</h2><p>这一章的 Recommended Problems:</p><ul><li>BSL P1 - More Arithmetic Expressions<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/more-arithmetic-expression-starter.rkt">more-arithmetic-expression-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/more-arithmetic-expression-solution.rkt">more-arithmetic-expression-solution.rkt</a></li></ul></li><li>BSL P3 - Tile<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/tile-starter.rkt">tile-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/tile-solution.rkt">tile-solution.rkt</a></li></ul></li><li>BSL P5 - Compare Images<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/compare-images-starter.rkt">compare-images-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/compare-images-solution.rkt">compare-images-solution.rkt</a></li></ul></li><li>BSL P6 - More Foo Evaluation<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/more-foo-evaluation-starter.rkt">more-foo-evaluation-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/more-foo-evaluation-solution.rkt">more-foo-evaluation-solution.rkt</a></li></ul></li><li>BSL P15 - Function Writing<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/function-writing-starter.rkt">function-writing-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/function-writing-solution.rkt">function-writing-solution.rkt</a></li></ul></li><li>BSL P16 - Foo Evaluation<ul><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/foo-evaluation-starter.rkt">foo-evaluation-starter.rkt</a></li><li><a href="https://s3.amazonaws.com/edx-course-spdx-kiczales/HTC/foo-evaluation-solution.rkt">foo-evaluation-solution.rkt</a></li></ul></li></ul><div class="tag-plugin colorful folders" ><details class="folder" index="0"><summary><p>BSL P1 - More Arithmetic Expressions 题解</p></summary><div class="body"><p><strong>预计耗时：5 min / 简单</strong></p><p>这道题让我们用两个表达式算出<code>3, 5, 7</code>的乘积。</p><p>第一个表达式应当为简单的三个数相乘，我们需要知道<code>*</code>是可以接受不止2个参数的：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">3</span> <span class="hljs-number">5</span> <span class="hljs-number">7</span>)<br></code></pre></td></tr></table></figure><p>第二个表达式即限制了<code>*</code>只接受两个参数，让我们嵌套着写，先算<code>3*5</code>，再算<code>15*7</code>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">7</span> (<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">3</span> <span class="hljs-number">5</span>))<br></code></pre></td></tr></table></figure></div></details><details class="folder" index="1"><summary><p>BSL P3 - Tile 题解</p></summary><div class="body"><p><strong>预计耗时：5 min / 简单</strong></p><p>这道题是让我们还原一个蓝黄矩形，我们可以考虑使用<code>above</code>和<code>beside</code>拼凑。为了方便，我们先定义一个黄色正方形和蓝色正方形：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> BLUE (<span class="hljs-name">rectangle</span> <span class="hljs-number">20</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> YELLOW (<span class="hljs-name">rectangle</span> <span class="hljs-number">20</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;yellow&quot;</span>))<br></code></pre></td></tr></table></figure><p>之后会发现这个矩形的拼凑逻辑可以是：</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:945/665;"><img class="lazy" src="/images/2025/academics-ubc-cpsc-110-beginning-student-language/practice-problems-bslp3.webp" data-src="/images/2025/academics-ubc-cpsc-110-beginning-student-language/practice-problems-bslp3.webp" alt="示意图" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div><div class="image-meta"><span class="image-caption center">示意图</span></div></div><p>实现代码如下：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">require</span></span> <span class="hljs-number">2</span>htdp/image)<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> BLUE (<span class="hljs-name">rectangle</span> <span class="hljs-number">20</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;blue&quot;</span>))<br>(<span class="hljs-name"><span class="hljs-built_in">define</span></span> YELLOW (<span class="hljs-name">rectangle</span> <span class="hljs-number">20</span> <span class="hljs-number">20</span> <span class="hljs-string">&quot;solid&quot;</span> <span class="hljs-string">&quot;yellow&quot;</span>))<br><br>(<span class="hljs-name">above</span> (<span class="hljs-name">beside</span> BLUE YELLOW) (<span class="hljs-name">beside</span> YELLOW BLUE))<br></code></pre></td></tr></table></figure></div></details><details class="folder" index="2"><summary><p>BSL P5 - Compare Images 题解</p></summary><div class="body"><p><strong>预计耗时：7 min / 简单</strong></p><p>这道题让我们对图像进行三次带有逻辑运算的比较。</p><p>第一个：判断<code>IMAGE1</code>是否比<code>IMAGE2</code>高？</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> (<span class="hljs-name">image-height</span> IMAGE1) (<span class="hljs-name">image-height</span> IMAGE2)) <br></code></pre></td></tr></table></figure><p>第二个：判断<code>IMAGE1</code>是否比<code>IMAGE2</code>窄 (宽度小)？</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">&lt;</span></span> (<span class="hljs-name">image-width</span> IMAGE1) (<span class="hljs-name">image-width</span> IMAGE2)) <br></code></pre></td></tr></table></figure><p>第三个：判断<code>IMAGE1</code>和<code>IMAGE2</code>的宽高是否都相同？</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">and</span></span> (<span class="hljs-name"><span class="hljs-built_in">=</span></span> (<span class="hljs-name">image-height</span> IMAGE1) (<span class="hljs-name">image-height</span> IMAGE2)) <br>     (<span class="hljs-name"><span class="hljs-built_in">=</span></span> (<span class="hljs-name">image-width</span> IMAGE1) (<span class="hljs-name">image-width</span> IMAGE2))<br>     )<br></code></pre></td></tr></table></figure></div></details><details class="folder" index="3"><summary><p>BSL P6 - More Foo Evaluation 题解</p></summary><div class="body"><p><strong>预计耗时：7 min / 简单</strong></p><p>比较讨厌的逐次人脑运算，从<code>(foo (+ 3 4))</code>开始：</p><ul><li>从运算顺序来讲，<code>(+ 3 4)</code>先被算出为<code>7</code>，得到<code>(foo 7)</code></li><li>之后它调用了<code>foo</code>函数，那么我们就把函数的内容（即<code>(* n n)</code>）复制到下面，并把<code>7</code>填进去，得到<code>(* 7 7)</code></li><li>最后计算<code>(* 7 7)</code>得到<code>49</code></li></ul><p>结果为：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">foo</span> (<span class="hljs-name"><span class="hljs-built_in">+</span></span> <span class="hljs-number">3</span> <span class="hljs-number">4</span>))<br><br>(<span class="hljs-name">foo</span> <span class="hljs-number">7</span>)<br><br>(<span class="hljs-name"><span class="hljs-built_in">*</span></span> <span class="hljs-number">7</span> <span class="hljs-number">7</span>)<br><br><span class="hljs-number">49</span><br></code></pre></td></tr></table></figure></div></details><details class="folder" index="4"><summary><p>BSL P15 - Function Writing 题解</p></summary><div class="body"><p><strong>预计耗时：5 min / 简单</strong></p><p>还是讨厌的逐次人脑运算，从<code>(foo (substring &quot;abcde&quot; 0 3))</code>开始：</p><ul><li>从运算顺序来讲，<code>(substring &quot;abcde&quot; 0 3)</code>先被算出为<code>&quot;abc&quot;</code>，得到<code>(foo &quot;abc&quot;)</code></li><li>之后它调用了<code>foo</code>函数，那么我们就把函数的内容复制到下面，并把<code>&quot;abc&quot;</code>填进去，得到：</li></ul><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> (<span class="hljs-name"><span class="hljs-built_in">substring</span></span> <span class="hljs-string">&quot;abc&quot;</span> <span class="hljs-number">0</span> <span class="hljs-number">1</span>) <span class="hljs-string">&quot;a&quot;</span>)<br>      (<span class="hljs-name"><span class="hljs-built_in">string-append</span></span> <span class="hljs-string">&quot;abc&quot;</span> <span class="hljs-string">&quot;a&quot;</span>)<br>      <span class="hljs-string">&quot;abc&quot;</span>)<br></code></pre></td></tr></table></figure><ul><li>这是个<code>if</code>表达式，我们先计算它的条件，即<code>(string=? (substring &quot;abc&quot; 0 1) &quot;a&quot;)</code>，得到：</li></ul><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> <span class="hljs-string">&quot;a&quot;</span> <span class="hljs-string">&quot;a&quot;</span>)<br>      (<span class="hljs-name"><span class="hljs-built_in">string-append</span></span> <span class="hljs-string">&quot;abc&quot;</span> <span class="hljs-string">&quot;a&quot;</span>)<br>      <span class="hljs-string">&quot;abc&quot;</span>)<br></code></pre></td></tr></table></figure><ul><li>这个条件的值为<code>true</code>，故：</li></ul><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">if</span></span> true<br>      (<span class="hljs-name"><span class="hljs-built_in">string-append</span></span> <span class="hljs-string">&quot;abc&quot;</span> <span class="hljs-string">&quot;a&quot;</span>)<br>      <span class="hljs-string">&quot;abc&quot;</span>)<br></code></pre></td></tr></table></figure><ul><li>那么我们就可以提取出<code>if</code>表达式条件为<code>true</code>时所运行的表达式了，即<code>(string-append &quot;abc&quot; &quot;a&quot;)</code></li><li>它的值是<code>&quot;abca&quot;</code></li></ul><p>结果为：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name">foo</span> (<span class="hljs-name"><span class="hljs-built_in">substring</span></span> <span class="hljs-string">&quot;abcde&quot;</span> <span class="hljs-number">0</span> <span class="hljs-number">3</span>))<br><br>(<span class="hljs-name">foo</span> <span class="hljs-string">&quot;abc&quot;</span>)<br><br>(<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> (<span class="hljs-name"><span class="hljs-built_in">substring</span></span> <span class="hljs-string">&quot;abc&quot;</span> <span class="hljs-number">0</span> <span class="hljs-number">1</span>) <span class="hljs-string">&quot;a&quot;</span>)<br>      (<span class="hljs-name"><span class="hljs-built_in">string-append</span></span> <span class="hljs-string">&quot;abc&quot;</span> <span class="hljs-string">&quot;a&quot;</span>)<br>      <span class="hljs-string">&quot;abc&quot;</span>)<br><br>(<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">string=?</span></span> <span class="hljs-string">&quot;a&quot;</span> <span class="hljs-string">&quot;a&quot;</span>)<br>      (<span class="hljs-name"><span class="hljs-built_in">string-append</span></span> <span class="hljs-string">&quot;abc&quot;</span> <span class="hljs-string">&quot;a&quot;</span>)<br>      <span class="hljs-string">&quot;abc&quot;</span>)<br><br>(<span class="hljs-name"><span class="hljs-built_in">if</span></span> true<br>      (<span class="hljs-name"><span class="hljs-built_in">string-append</span></span> <span class="hljs-string">&quot;abc&quot;</span> <span class="hljs-string">&quot;a&quot;</span>)<br>      <span class="hljs-string">&quot;abc&quot;</span>)<br><br>(<span class="hljs-name"><span class="hljs-built_in">string-append</span></span> <span class="hljs-string">&quot;abc&quot;</span> <span class="hljs-string">&quot;a&quot;</span>)<br><br><span class="hljs-string">&quot;abca&quot;</span><br></code></pre></td></tr></table></figure></div></details><details class="folder" index="5"><summary><p>BSL P16 - Foo Evaluation 题解</p></summary><div class="body"><p><strong>预计耗时：15 min / 中等</strong></p><p>这道题需要自定义一个函数：接受两个数，返回较大的数。也就是说这道题需要用到<code>if</code>表达式，那么我们可以先构思这个条件判断：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> a b)  <span class="hljs-comment">; 判断 a 是否大于 b</span><br>     a  <span class="hljs-comment">; 大于就输出 a</span><br>     b  <span class="hljs-comment">; 否则就输出 b</span><br>     )<br></code></pre></td></tr></table></figure><p>之后我们将这个<code>if</code>表达式放进一个<code>define</code>里面，将该函数命名为<code>foo</code> <em>（其他什么的也可以）</em>：</p><figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scheme">(<span class="hljs-name"><span class="hljs-built_in">define</span></span> (<span class="hljs-name">foo</span> a b)<br>  (<span class="hljs-name"><span class="hljs-built_in">if</span></span> (<span class="hljs-name"><span class="hljs-built_in">&gt;</span></span> a b) a b))<br></code></pre></td></tr></table></figure></div></details></div>]]>
    </content>
    <id>https://ziling.moe/2025/academics-ubc-cpsc-110-beginning-student-language/</id>
    <link href="https://ziling.moe/2025/academics-ubc-cpsc-110-beginning-student-language/"/>
    <published>2025-07-01T15:30:00.000Z</published>
    <summary>UBC 的计科大一必修课 - CPSC 110</summary>
    <title>UBC - CPSC 110 - Beginning Student Language</title>
    <updated>2025-07-01T15:30:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term="Academics" scheme="https://ziling.moe/categories/Academics/"/>
    <category term="UBC" scheme="https://ziling.moe/tags/UBC/"/>
    <category term="计算机科学" scheme="https://ziling.moe/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6/"/>
    <category term="教程" scheme="https://ziling.moe/tags/%E6%95%99%E7%A8%8B/"/>
    <content>
      <![CDATA[<p>这门课作为所有打算进入CS专业的必修课之一，虽然搞不懂为什么要用 Racket 语言执教，但如果是为了锻炼编程思维，还是苦一苦自己吧。</p><span id="more"></span><p><em>ps: Racket 真是一个冷门的语言，在了解这门课程之前我甚至不知道</em></p><p>BTW，这门课等价于<a href="https://learning.edx.org/course/course-v1:UBCx+HtC1x+2T2017/home">edX 上面的 How to Code: Simple Data 免费版</a>，之后的笔记会基于该课程进行，<em>配合edX视频风味更佳</em>。</p><p>有其他编程语言（如 Python）基础的读者会更易懂，即笔者有时会使用 Python 作为伪代码解释本课程主要使用的编程语言 —— <a href="https://racket-lang.org/">Racket</a></p><p><a href="https://cs110.students.cs.ubc.ca/admin/setup.html">如何安装 Racket？</a></p><p>在开始之前，需要了解下这门课程的目标：</p><ul><li>写代码解决问题</li><li>写出可读性强、结构良好、文档齐全和经过测试的代码</li><li>通过数据获取信息</li><li>程序是如何操作不同数据结构、数据类型的</li><li>代码的抽象（可用于设计库），简化相似解决方案的代码</li><li>在程序设计中使用非代码模型</li></ul><p>作为有过一些 C#/Python 开发经验的我来说，我似乎没有思考过数据与信息的关系，以及所谓“非代码模型”是什么。</p><p>但问题不大，课程的内容会在后续的学习中逐渐展开。</p><p>程序设计中请遵循以下要点：</p><ul><li>跑通≠跑得好：你需要解释你怎么写的，哪来的自信保证它跑得好和复现？</li><li>软件开发是个团队工作：你写的代码可能之后会被其他人修改，所以你要时刻保持友好。 <em>（我不认为）</em></li><li>脚踏实地：不要总是指望灵感迸发，请始终以专业地态度来对待你的代码。</li><li>程序以数据表达信息：信息无处不在，数据是信息的载体，程序需要处理数据。</li><li>程序是有结构的：初级的结构包括条件判断、循环等，复杂一点的参考<a href="https://refactoring.guru/design-patterns/what-is-pattern">设计模式</a>。</li></ul><p>在系统性程序设计中，请：</p><ul><li>写一致的、通过测试的代码。</li><li>基于对不同编程语言和软件工程的研究与实践的学习。</li></ul><p>或许你可以从AIGC中复制代码，但你如何：</p><ul><li>解释你怎么开发出来的？</li><li>证明它是正确的？</li><li>写出可靠的类似程序？</li></ul><p><em>ps: 从AI抄代码是十分严重的学术不端的行为</em></p><p>世界上有成百上千的编程语言，其中有几百个较为熟知，但没有一个编程语言是最好的，能完美应对所有场景的。</p><p>借此，初学者可以通过学习 BSL (Beginning Student Language) 来了解编程语言的基本概念，以便专注于学习系统性程序设计、在未来更快地学习其他语言。</p><p>为了达到学习目的，这节课主要通过解决设计好的问题来进行，无论是 Lecture/Lab/Problem Sets/Homework</p><p>这节课不需要多少数学等STEM学科基础，但你需要足够细心（应对程序报错）、耐心（长时间解决一道难题）、谦虚。（看着简单的题目也有可能做着难）</p>]]>
    </content>
    <id>https://ziling.moe/2025/academics-ubc-cpsc-110-introduction/</id>
    <link href="https://ziling.moe/2025/academics-ubc-cpsc-110-introduction/"/>
    <published>2025-07-01T15:30:00.000Z</published>
    <summary>
      <![CDATA[<p>这门课作为所有打算进入CS专业的必修课之一，虽然搞不懂为什么要用 Racket 语言执教，但如果是为了锻炼编程思维，还是苦一苦自己吧。</p>]]>
    </summary>
    <title>UBC - CPSC 110 - Introduction</title>
    <updated>2025-07-01T15:30:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term="Nymph" scheme="https://ziling.moe/categories/Nymph/"/>
    <category term="MtF" scheme="https://ziling.moe/tags/MtF/"/>
    <category term="护理" scheme="https://ziling.moe/tags/%E6%8A%A4%E7%90%86/"/>
    <content>
      <![CDATA[<details class="tag-plugin colorful folding" color="blue" open><summary><p>前言</p></summary><div class="body"><ul> <li>本文基于 <a href="https://zhuanlan.zhihu.com/p/22714893581">给跨性别者的一些关于护肤气质的建议</a> 进行二次创作，内容仅供参考，不构成任何医疗建议。</li> <li>文中提到的产品和药物推荐均为原作者个人意见，不构成广告推广。</li> </ul> </div></details><h2 id="皮肤护理"><a class="markdownIt-Anchor" href="#皮肤护理"></a> 皮肤护理</h2><h3 id="药物"><a class="markdownIt-Anchor" href="#药物"></a> 药物</h3><h4 id="维生素c"><a class="markdownIt-Anchor" href="#维生素c"></a> 维生素C</h4><p>东北制药维生素C片（5-6r/瓶），每日1片/100mg口服。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:710/726;width:200px;"><img class="lazy" src="/images/2025/mtf-advice/vitamin-c.webp" data-src="/images/2025/mtf-advice/vitamin-c.webp" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div><details class="tag-plugin colorful folding" color="gray" open><summary><p>作用</p></summary><div class="body"><ul> <li>抗氧化保护：减少紫外线引起的皮肤损伤，预防光老化（如皱纹、色斑等），并延缓皮肤的自然老化过程。</li> <li>促进胶原蛋白合成：增强皮肤的弹性和紧致度，减少皱纹和细纹，帮助维持皮肤的年轻状态。</li> <li>抑制黑色素生成：改善肤色不均，减少色斑和暗沉，使皮肤更加明亮。</li> <li>促进皮肤屏障功能：改善皮肤干燥问题，增强皮肤的保湿能力，减少水分流失。</li> </ul> </div></details><details class="tag-plugin colorful folding" color="orange" open><summary><p>注意</p></summary><div class="body"><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:918/950;width:300px;"><img class="lazy" src="/images/2025/mtf-advice/vitamin-c-recommend.webp" data-src="/images/2025/mtf-advice/vitamin-c-recommend.webp" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div> <p>维生素C的成人推荐摄入量：</p> <ul> <li>45mg/天或300mg/周 (WHO)</li> <li>100mg/天（中国居民膳食营养素参考摄入量）</li> </ul> <p>安全剂量：低于2000mg/天。</p> <p>参考食物摄入量：1个石榴/2个橙子。</p> </div></details><h4 id="维生素e"><a class="markdownIt-Anchor" href="#维生素e"></a> 维生素E</h4><p>维生素E软胶囊（6-10r/瓶），每日1粒/100mg口服。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:800/800;width:200px;"><img class="lazy" src="/images/2025/mtf-advice/vitamin-e.webp" data-src="/images/2025/mtf-advice/vitamin-e.webp" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div><details class="tag-plugin colorful folding" color="gray" open><summary><p>作用</p></summary><div class="body"><ul> <li>抗氧化保护：清除自由基和活性氧来保护皮肤免受损伤。</li> <li>增强紫外线防护：单独使用时，对紫外线的防护效果有限，但与维生素C联合使用时，可以显著增强皮肤的光防护能力。</li> <li>抗炎作用：抑制紫外线诱导的炎症介质的产生，减少紫外线诱导的皮肤炎症反应。</li> <li>减少光老化：减少紫外线诱导的光老化，包括减少皱纹、色素沉着和皮肤松弛。</li> </ul> </div></details><details class="tag-plugin colorful folding" color="orange" open><summary><p>注意</p></summary><div class="body"><p>维生素E的成人推荐摄入量：100mg/天。</p> <p>安全剂量：低于200-400mg/天。</p> </div></details><h4 id="维生素b3-烟酰胺"><a class="markdownIt-Anchor" href="#维生素b3-烟酰胺"></a> 维生素B3 (烟酰胺)</h4><p>复合维生素B片（5-10r/瓶），每日1片口服。</p><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:600/600;width:200px;"><img class="lazy" src="/images/2025/mtf-advice/vitamin-b.webp" data-src="/images/2025/mtf-advice/vitamin-b.webp" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div><details class="tag-plugin colorful folding" color="gray" open><summary><p>作用</p></summary><div class="body"><ul> <li><emp>护肝</emp></li> <li>增强皮肤屏障：促进角质层形成，增强皮肤的保水能力，减少经皮水分流失，维护皮肤屏障的完整性。</li> <li>改善皮肤色素沉着：通过抑制黑色素合成酶的活性，减少黑色素的生成，并阻断黑色素向表皮细胞的转移，维生素B3可以有效减轻色斑和肤色不均，达到美白效果。</li> <li>抗炎与舒缓敏感：减轻皮肤炎症反应，缓解因痘痘、晒伤等引起的红肿和瘙痒，适合敏感性皮肤使用。</li> <li>促进皮肤修复与再生：促进表皮细胞的生长和分化，加速受损皮肤的修复，维持皮肤的正常功能。</li> <li>调节皮脂分泌：调节皮肤的油脂分泌，减少皮脂过度生成，从而改善毛孔粗大和痤疮问题。</li> <li>抗衰老与抗氧化：刺激胶原蛋白生成，减少皱纹，同时具有抗氧化作用，保护皮肤免受自由基的损害。</li> </ul> </div></details><details class="tag-plugin colorful folding" color="orange" open><summary><p>非处方药</p></summary><div class="body"><p>即使以上药物为非处方药，使用前仍建议咨询医生。</p> <p>注意药物的摄入量和服用后的不良反应，如过敏、头晕、恶心、腹泻等。</p> </div></details><h4 id="附录-医保"><a class="markdownIt-Anchor" href="#附录-医保"></a> 附录: 医保</h4><p>上文提到的维生素C、维生素E、维生素B3均为非处方药，可以通过医保报销。</p><p><a href="https://www.gov.cn/zhengce/zhengceku/202411/P020241128820415409368.pdf">国家基本医疗保险、工伤保险和生育保险药品目录（2024年）</a></p><h3 id="面部"><a class="markdownIt-Anchor" href="#面部"></a> 面部</h3><h4 id="补水喷雾"><a class="markdownIt-Anchor" href="#补水喷雾"></a> 补水喷雾</h4><p>平价解决方案：依云矿泉水/屈臣氏蒸馏水/娃哈哈纯净水+喷瓶</p><div class="tag-plugin tabs" align="center"id="tab_7"><div class="nav-tabs"><div class="tab active"><a href="#tab_7-1">依云矿泉水</a></div><div class="tab"><a href="#tab_7-2">屈臣氏蒸馏水</a></div><div class="tab"><a href="#tab_7-3">娃哈哈纯净水</a></div></div><div class="tab-content"><div class="tab-pane active" id="tab_7-1"><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:800/800;width:200px;"><img class="lazy" src="/images/2025/mtf-advice/evian.webp" data-src="/images/2025/mtf-advice/evian.webp" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div></div><div class="tab-pane" id="tab_7-2"><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:555/740;width:200px;"><img class="lazy" src="/images/2025/mtf-advice/watsons.webp" data-src="/images/2025/mtf-advice/watsons.webp" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div></div><div class="tab-pane" id="tab_7-3"><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:480/480;width:200px;"><img class="lazy" src="/images/2025/mtf-advice/wahaha.webp" data-src="/images/2025/mtf-advice/wahaha.webp" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div></div></div></div><details class="tag-plugin colorful folding" color="blue" open><summary><p>使用建议</p></summary><div class="body"><p>喷的时候要离脸部15-20cm，均匀覆盖，之后用手轻轻拍打，帮助水分吸收。</p> </div></details><h4 id="芦荟胶"><a class="markdownIt-Anchor" href="#芦荟胶"></a> 芦荟胶</h4><p>推荐成分：优先库拉索芦荟</p><details class="tag-plugin colorful folding" color="blue" open><summary><p>使用建议</p></summary><div class="body"><p>清洁面部后，取适量芦荟胶（约黄豆大小）涂抹于脸部，轻轻按摩至吸收。</p> <p>在出现痘痘、晒伤、粉刺等情况时，可局部加量使用。</p> </div></details><h4 id="洗面奶"><a class="markdownIt-Anchor" href="#洗面奶"></a> 洗面奶</h4><emp>洗面奶的效果因人而异，建议前期多买多试，同时注意可能出现的过敏不适情况</emp><details class="tag-plugin colorful folding" color="blue" open><summary><p>使用建议</p></summary><div class="body"><p>洗脸时，取适量洗面奶在手心，<emp>加水揉搓起泡</emp>，然后涂抹于脸部（同时注意下巴和耳朵周围），轻轻按摩后用清水洗净。</p> </div></details><h4 id="推荐步骤"><a class="markdownIt-Anchor" href="#推荐步骤"></a> 推荐步骤</h4><ol><li>清水湿润面部</li><li>洗面奶洗脸</li><li>用毛巾轻轻擦干</li><li>喷雾补水</li><li>毛孔粗大情况</li></ol><ul><li>早上，在毛孔粗大的部位涂抹<emp>水杨酸</emp>，用手进行按摩，5到10分钟之后清洗掉。</li><li>晚上，在毛孔粗大的部位涂抹<emp>壬二酸</emp>，用手进行按摩，5到10分钟之后清洗掉。</li></ul><ol start="6"><li>敷芦荟胶15分钟左右，拍打至吸收</li><li>洗脸</li></ol><details class="tag-plugin colorful folding" color="orange" open><summary><p>水杨酸&amp;壬二酸注意</p></summary><div class="body"><ul> <li>使用频率：每周2-3次</li> <li>不耐受：如果出现过敏不适超过1周以上，立即停止使用，必要时就医</li> </ul> </div></details><h3 id="身体"><a class="markdownIt-Anchor" href="#身体"></a> 身体</h3><p>可以通过身体乳进行皮肤保湿。</p><emp>每个人对身体乳的体验不同，建议多试，同时注意可能出现的过敏不适情况</emp><div class="tag-plugin tabs" align="center"id="tab_8"><div class="nav-tabs"><div class="tab active"><a href="#tab_8-1">韩国所望牛奶身体乳</a></div><div class="tab"><a href="#tab_8-2">雪玲妃烟酰胺樱花身体乳</a></div></div><div class="tab-content"><div class="tab-pane active" id="tab_8-1"><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:800/800;width:300px;"><img class="lazy" src="/images/2025/mtf-advice/body-lotion-1.webp" data-src="/images/2025/mtf-advice/body-lotion-1.webp" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div></div><div class="tab-pane" id="tab_8-2"><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:480/480;width:300px;"><img class="lazy" src="/images/2025/mtf-advice/body-lotion-2.webp" data-src="/images/2025/mtf-advice/body-lotion-2.webp" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div></div></div></div><details class="tag-plugin colorful folding" color="blue" open><summary><p>使用建议</p></summary><div class="body"><p>洗澡后擦干全身，取适量身体乳涂抹，轻轻按摩+拍打至吸收。</p> </div></details><h3 id="手部"><a class="markdownIt-Anchor" href="#手部"></a> 手部</h3><h4 id="护手霜"><a class="markdownIt-Anchor" href="#护手霜"></a> 护手霜</h4><emp>推荐线下购买，注意可能出现的过敏不适情况</emp><div class="tag-plugin tabs" align="center"id="tab_9"><div class="nav-tabs"><div class="tab active"><a href="#tab_9-1">安野屋</a></div><div class="tab"><a href="#tab_9-2">屈臣氏香氛手霜</a></div><div class="tab"><a href="#tab_9-3">屈臣氏植萃精油</a></div></div><div class="tab-content"><div class="tab-pane active" id="tab_9-1"><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:800/800;width:300px;"><img class="lazy" src="/images/2025/mtf-advice/hand-cream-1.webp" data-src="/images/2025/mtf-advice/hand-cream-1.webp" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div></div><div class="tab-pane" id="tab_9-2"><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:800/800;width:300px;"><img class="lazy" src="/images/2025/mtf-advice/hand-cream-2.webp" data-src="/images/2025/mtf-advice/hand-cream-2.webp" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div></div><div class="tab-pane" id="tab_9-3"><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:892/806;width:300px;"><img class="lazy" src="/images/2025/mtf-advice/hand-cream-3.webp" data-src="/images/2025/mtf-advice/hand-cream-3.webp" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div></div></div></div><h4 id="指甲"><a class="markdownIt-Anchor" href="#指甲"></a> 指甲</h4><ul><li>修剪成圆形或方形，用指甲锉打磨光滑</li><li>抛光 (可选)</li></ul><h2 id="体重管理"><a class="markdownIt-Anchor" href="#体重管理"></a> 体重管理</h2><p><em>ps: 这部分内容为本站笔者本人实际体验</em></p><h3 id="健康体重标准"><a class="markdownIt-Anchor" href="#健康体重标准"></a> 健康体重标准</h3><p>BMI范围：18-24 kg/m²</p><p><a href="https://www.calculator.net/bmi-calculator.html">BMI Calculator</a></p><p>如需减肥，建议每周测量一次体重。</p><h3 id="饮食建议"><a class="markdownIt-Anchor" href="#饮食建议"></a> 饮食建议</h3><p>对于BMI超标的情况，控制饮食远比运动更有效。</p><ul><li><emp>戒糖：减少糖分摄入，避免饮料和甜食</emp></li><li>少油少盐：减少油脂和盐分摄入</li><li><emp>水分摄入：每日1500-2000ml</emp></li><li>饮食法：16:8禁食法（每日8小时进食，16小时禁食）<emp>(可选择更适合自己的)</emp></li><li>饱腹感：一顿饭后不超过7-8分饱</li></ul><h3 id="运动建议"><a class="markdownIt-Anchor" href="#运动建议"></a> 运动建议</h3><p>推荐平台：BiliBili、Keep</p><h4 id="大基数"><a class="markdownIt-Anchor" href="#大基数"></a> 大基数</h4><emp>注意：经验观察表明，运动并不能作为减肥的主要手段，饮食控制才是关键</emp><p>考虑到大基数的情况，建议选择膝盖友好/低冲击的有氧运动。</p><p>推荐运动：快走、爬楼梯、游泳 <em>确保姿势正确</em></p><p>以爬楼梯为例，建议每日爬楼梯30分钟，每周4-5次。</p><h4 id="正常范围"><a class="markdownIt-Anchor" href="#正常范围"></a> 正常范围</h4><p>到达正常范围后，运动目的可以转变为塑形。</p><p>推荐：</p><ul><li><a href="https://space.bilibili.com/604003146">帕梅拉PamelaReif</a></li><li>美丽芭蕾</li></ul><h2 id="毛发护理"><a class="markdownIt-Anchor" href="#毛发护理"></a> 毛发护理</h2><table><thead><tr><th>方式</th><th>使用频率</th><th>疼痛</th><th>注意</th></tr></thead><tbody><tr><td>剃须刀</td><td>1-2周/次</td><td>★☆☆☆☆</td><td>使用前打湿皮肤</td></tr><tr><td>脱毛膏</td><td>应急，不建议长期使用</td><td>★☆☆☆☆</td><td>注意过敏可能</td></tr><tr><td>在家激光</td><td>2周/次 -&gt; 2-3月/次</td><td>★★★☆☆</td><td>使用前涂抹防晒霜</td></tr><tr><td>医疗激光</td><td>几乎永久</td><td>★★★★☆</td><td>需要去3-5次</td></tr></tbody></table><p><em>可使用修眉刀修剪眉毛，更细/薄一点</em></p><h2 id="仪态塑造"><a class="markdownIt-Anchor" href="#仪态塑造"></a> 仪态塑造</h2><p><em>仪态塑造不是必须的，活出自己的样子才是最重要的，此处不作推荐，可以从以下角度探索</em></p><ul><li>坐姿、站姿、走姿</li><li>行为举止、礼仪礼节</li></ul><h2 id="嘴唇与口腔"><a class="markdownIt-Anchor" href="#嘴唇与口腔"></a> 嘴唇与口腔</h2><h3 id="嘴唇"><a class="markdownIt-Anchor" href="#嘴唇"></a> 嘴唇</h3><div class="tag-plugin image"><div class="image-bg" style="aspect-ratio:600/450;width:300px;"><img class="lazy" src="/images/2025/mtf-advice/lips.webp" data-src="/images/2025/mtf-advice/lips.webp" data-fancybox="true"onerror="this.src=&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2rem' height='2rem' viewBox='0 0 24 24'%3E%3C!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ --%3E%3Cpath fill='%23F44336' d='M22 12.698c-.002 1.47-.013 2.718-.096 3.743c-.097 1.19-.296 2.184-.74 3.009a4.2 4.2 0 0 1-.73.983c-.833.833-1.893 1.21-3.237 1.39C15.884 22 14.2 22 12.053 22h-.106c-2.148 0-3.83 0-5.144-.177c-1.343-.18-2.404-.557-3.236-1.39c-.738-.738-1.12-1.656-1.322-2.795c-.2-1.12-.236-2.512-.243-4.241Q1.999 12.737 2 12v-.054c0-2.148 0-3.83.177-5.144c.18-1.343.557-2.404 1.39-3.236s1.893-1.21 3.236-1.39c1.168-.157 2.67-.175 4.499-.177a.697.697 0 1 1 0 1.396c-1.855.002-3.234.018-4.313.163c-1.189.16-1.906.464-2.436.994S3.72 5.8 3.56 6.99C3.397 8.2 3.395 9.788 3.395 12v.784l.932-.814a2.14 2.14 0 0 1 2.922.097l3.99 3.99a1.86 1.86 0 0 0 2.385.207l.278-.195a2.79 2.79 0 0 1 3.471.209l2.633 2.37c.265-.557.423-1.288.507-2.32c.079-.972.09-2.152.091-3.63a.698.698 0 0 1 1.396 0' opacity='.5'/%3E%3Cpath fill='%23F44336' fill-rule='evenodd' d='M17.5 11c-2.121 0-3.182 0-3.841-.659S13 8.621 13 6.5s0-3.182.659-3.841S15.379 2 17.5 2s3.182 0 3.841.659S22 4.379 22 6.5s0 3.182-.659 3.841S19.621 11 17.5 11m-1.47-7.03a.75.75 0 1 0-1.06 1.06l1.47 1.47l-1.47 1.47a.75.75 0 0 0 1.06 1.06l1.47-1.47l1.47 1.47a.75.75 0 1 0 1.06-1.06L18.56 6.5l1.47-1.47a.75.75 0 0 0-1.06-1.06L17.5 5.44z' clip-rule='evenodd'/%3E%3C/svg%3E&quot;"/><div class="lazy-icon" style="background-image:url(https://api.iconify.design/eos-icons:three-dots-loading.svg?color=%231cd0fd);"></div></div></div><details class="tag-plugin colorful folding" color="blue" open><summary><p>提示</p></summary><div class="body"><p>可选用带颜色的唇膏替代口红，日常涂抹</p> </div></details><h3 id="口腔"><a class="markdownIt-Anchor" href="#口腔"></a> 口腔</h3><ul><li>刷牙：每天1次 或 早晚各1次</li><li>正畸 <em>(如需)</em></li></ul><h2 id="视力保护"><a class="markdownIt-Anchor" href="#视力保护"></a> 视力保护</h2><table><thead><tr><th>产品</th><th>使用频率</th><th>有效性</th><th>备注</th></tr></thead><tbody><tr><td>蒸汽眼罩</td><td>1周不超过4次</td><td>★☆☆☆☆</td><td>短时间解决视疲劳问题</td></tr><tr><td>眼药水</td><td>干涩时/1天3次左右 (不超过6次)</td><td>★★★☆☆</td><td>建议带玻璃酸钠成分</td></tr><tr><td>叶黄素</td><td>每天20mg</td><td>★★★☆☆</td><td>不要选成叶黄素酯</td></tr></tbody></table><h2 id="精神心理"><a class="markdownIt-Anchor" href="#精神心理"></a> 精神&amp;心理</h2><h3 id="量表"><a class="markdownIt-Anchor" href="#量表"></a> 量表</h3><emp>量表仅供参考，不构成诊断依据，有效性低于个人主观生活体验</emp><ul><li>SDS 抑郁自评量表</li><li>SAS 焦虑自评量表</li><li>SCL-90 症状自评量表</li></ul><h3 id="就医"><a class="markdownIt-Anchor" href="#就医"></a> 就医</h3><p>如需检查精神疾病，建议就医 (精神科/心理科/临床心理科等)。</p><p>部分医院会提供脑检查，可以了解大脑内神经递质是否正常。<em>更客观</em></p><h3 id="心理援助热线"><a class="markdownIt-Anchor" href="#心理援助热线"></a> 心理援助热线</h3><div class="tag-plugin blockquote" indent="undefined"><p>不向焦虑与抑郁投降，这个世界终会有我们存在的地方。<br />如果你能记住我的名字，如果你们都能记住我的名字，也许我或者“我们”，终有一天能自由地生存着。<br />—— MtF.wiki</p></div><h4 id="全国"><a class="markdownIt-Anchor" href="#全国"></a> 全国</h4><ul><li><emp>全国统一心理援助热线电话号码: 12356</emp> *[国卫医政函〔2024〕259号](http://www.nhc.gov.cn/cms-search/xxgk/getManuscriptXxgk.htm)*</li><li>全国公共卫生公益电话：12320 <em><a href="http://www.nhc.gov.cn/bgt/pzhgli/200806/8b0dc77cb7fb44b4b870b821c98bf575.shtml">卫办发〔2005〕486号</a></em></li><li>青少年服务台：12355</li><li>妇女维权公益服务热线：12338</li></ul><h4 id="地区"><a class="markdownIt-Anchor" href="#地区"></a> 地区</h4><details class="tag-plugin colorful folding" color="blue"><summary><p>各地心理援助热线</p></summary><div class="body"><p><emp>北京</emp></p> <ul> <li>北京市心理援助热线：010-82951332</li> <li>北京安定医院心理咨询热线：010-58303286</li> <li>“启明灯”中国科学院大学心理援助热线：4006525580</li> </ul> <p><emp>天津</emp></p> <ul> <li>天津市心理援助热线：022-88188858</li> </ul> <p><emp>河北</emp></p> <ul> <li>河北省心理援助热线：0312-96312</li> <li>衡水市心理援助热线：0318-2117120</li> </ul> <p><emp>内蒙古</emp></p> <ul> <li>内蒙古自治区心理援助热线：0471-12320转5</li> <li>包头市第六医院心理援助热线：0472-2625521</li> </ul> <p><emp>上海</emp></p> <ul> <li>上海市心理咨询预约：021-51699291</li> <li>上海市心理热线：021-962525</li> </ul> <p><emp>江苏</emp></p> <ul> <li>江苏省心理危机干预热线：025-83712977</li> <li>南京市危机干预中心：025-86528082</li> </ul> <p><emp>浙江</emp></p> <ul> <li>杭州市心理危机热线：0571-85029595</li> </ul> <p><emp>山东</emp></p> <ul> <li>山东省精神卫生中心心理援助热线：0531-86336666</li> <li>青岛市心理援助热线：0532—85669120</li> </ul> <p><emp>山西</emp></p> <ul> <li>山西省心理援助热线：0351-8726199</li> </ul> <p><emp>安徽</emp></p> <ul> <li>合肥市第四人民医院：0551-63666903</li> <li>黄山市第二人民医院：0559-2591072</li> </ul> <p><emp>湖北</emp></p> <ul> <li>湖北省社会心理学会心理援助热线：027-87832211</li> <li>武汉市精神卫生中心：027-85844666</li> </ul> <p><emp>湖南</emp></p> <ul> <li>心理健康服务热线：0731-85292999</li> </ul> <p><emp>江西</emp></p> <ul> <li>江西省社会心理服务热线：0791-966525</li> </ul> <p><emp>河南</emp></p> <ul> <li>河南省心理援助热线：0373-7095888</li> </ul> <p><emp>广东</emp></p> <ul> <li>广州市心理援助热线：020-81899120</li> <li>广东省心理援助公益：020-81777652</li> </ul> <p><emp>广西</emp></p> <ul> <li>广西心理援助热线：0772-3136120</li> </ul> <p><emp>福建</emp></p> <ul> <li>泉州市心理援助热线：0595-968908</li> </ul> <p><emp>黑龙江</emp></p> <ul> <li>黑龙江省危机干预热线：0451-12320</li> </ul> <p><emp>吉林</emp></p> <ul> <li>长春市心理危机援助热线：0431-89685000</li> </ul> <p><emp>辽宁</emp></p> <ul> <li>辽宁省心理援助热线：024-96687</li> <li>辽宁省精神卫生中心援助热线：024-73377120</li> </ul> <p><emp>四川</emp></p> <ul> <li>四川省心理援助热线：96111</li> <li>成都市心理援助热线：028-87577510</li> </ul> <p><emp>重庆</emp></p> <ul> <li>重庆市心理援助热线：023-96320-1</li> </ul> <p><emp>贵州</emp></p> <ul> <li>贵州省心理援助热线：0851-88417888</li> </ul> <p><emp>云南</emp></p> <ul> <li>昆明市心理援助热线：0871-6501111</li> </ul> <p><emp>西藏</emp></p> <ul> <li>西藏自治区心理咨询热线：0891-12320</li> </ul> <p><emp>陕西</emp></p> <ul> <li>西安市精神卫生中心心理援助热线：400-8960960</li> <li>陕西省精神卫生中心咨询电话：029-63609288</li> </ul> <p><emp>青海</emp></p> <ul> <li>青海省第三人民医院心理咨询热线：0971-8140371</li> </ul> <p><emp>甘肃</emp></p> <ul> <li>兰州市心理援助热线：0931-4638858</li> </ul> <p><emp>宁夏</emp></p> <ul> <li>宁夏心理援助热线：0951-2160707</li> </ul> <p><emp>新疆</emp></p> <ul> <li>新疆心理援助热线：0991-3016111</li> </ul> <p><emp>海南</emp></p> <ul> <li>海南省心理援助热线：0898-96363</li> </ul> </div></details><h2 id="更多信息"><a class="markdownIt-Anchor" href="#更多信息"></a> 更多信息</h2><ul><li><a href="https://mtf.wiki/zh-cn/">Project Trans/MtF.wiki</a></li></ul>]]>
    </content>
    <id>https://ziling.moe/2025/mtf-advice/</id>
    <link href="https://ziling.moe/2025/mtf-advice/"/>
    <published>2025-02-14T14:00:00.000Z</published>
    <summary>本文基于 给跨性别者的一些关于护肤气质的建议 进行二次创作，内容仅供参考，不构成任何医疗建议。</summary>
    <title>MtF 护理建议</title>
    <updated>2025-02-14T14:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term=".NET Learning" scheme="https://ziling.moe/categories/NET-Learning/"/>
    <category term=".NET" scheme="https://ziling.moe/tags/NET/"/>
    <category term="翻译" scheme="https://ziling.moe/tags/%E7%BF%BB%E8%AF%91/"/>
    <category term="转载" scheme="https://ziling.moe/tags/%E8%BD%AC%E8%BD%BD/"/>
    <content>
      <![CDATA[<p>本文翻译自 <a href="https://devblogs.microsoft.com/dotnet/why-dotnet">What is .NET, and why should you choose it?</a></p><span id="more"></span><p>原文作者尾注：This post was written by <a href="https://github.com/jkotas">Jan Kotas</a>, <a href="https://github.com/richlander">Rich Lander</a>, <a href="https://github.com/Maoni0">Maoni Stephens</a>, and <a href="https://github.com/stephentoub">Stephen Toub</a>, with the insight and review of our colleagues on the .NET team.</p><details class="tag-plugin colorful folding" color="blue" open><summary><p>声明</p></summary><div class="body"><ul> <li>翻译会在易读易懂的基础上略微修改原文的分段和用词</li> <li>出现专用名词或是难以翻译的地方会用括号标注原词</li> <li>想额外补充的地方会使用橙色卡片标注</li> </ul> </div></details><h2 id="前言"><a class="markdownIt-Anchor" href="#前言"></a> 前言</h2><p>自从我们启动这个开源的跨平台项目以来，<a href="https://github.com/dotnet/runtime">.NET</a> 已经快速发展并发生了巨大的变化。我们重新构思并打磨了该平台，增加了许多底层功能以提高性能和安全性，同时也引入了很多提升开发效率的高级特性，比如，<a href="https://learn.microsoft.com/archive/msdn-magazine/2018/january/csharp-all-about-span-exploring-a-new-net-mainstay"><code>Span&lt;T&gt;</code></a>、<a href="https://devblogs.microsoft.com/dotnet/hardware-intrinsics-in-net-core/">硬件内在函数 (Hardware Intrinsics)</a>、<a href="https://learn.microsoft.com/dotnet/csharp/nullable-references">可空引用类型</a>等。现在我们推出一个新的博客系列，叫做“.NET Design Point”，以深入探讨当今 .NET 平台的基础概念和设计，以及这些设计如何为你当前编写的代码带来好处。</p><p>该系列的第一篇文章概述了 .NET 平台的支柱和设计理念：当你选择 .NET 时，在基础层面上“你能获得什么”。这篇文章旨在为你提供一个足够清晰、基于事实的框架，以便你可以用它向其他人介绍这个平台。后续的文章将深入讨论这些主题，因为仅靠本篇文章还不足以详尽地展示这些特性。同时，本篇不会涉及相关工具，例如 Visual Studio，也不会谈及像 <a href="http://ASP.NET">ASP.NET</a> 框架提供的库和应用模型。</p><p>后续文章：</p><ul><li><a href="https://devblogs.microsoft.com/dotnet/how-async-await-really-works/">How Async/Await Really Works in C#</a></li><li><a href="https://devblogs.microsoft.com/dotnet/the-convenience-of-dotnet">The convenience of .NET</a></li></ul><p>在深入探讨之前，值得先聊一聊 .NET 的使用情况。目前，有数百万开发者使用 .NET 来开发运行在<a href="https://github.com/dotnet/core/blob/main/release-notes/7.0/supported-os.md">多种操作系统和不同芯片架构</a>上的云端、客户端以及其他类型的应用程序。它也运行在一些大家耳熟能详的地方，比如 <a href="https://azure.microsoft.com/">Azure</a>、<a href="https://wouterdekort.com/2022/05/25/the-stackoverflow-journey-to-dotnet6/">StackOverflow</a> 和 <a href="https://blog.unity.com/technology/unity-and-net-whats-next">Unity</a>。尤其是在大型公司中，你会看到 .NET 的广泛应用。在很多地方，掌握 .NET 是一项非常有价值的技能，有助于你找到理想的工作。</p><h2 id="net-设计要点"><a class="markdownIt-Anchor" href="#net-设计要点"></a> .NET 设计要点</h2><div class="tag-plugin blockquote" indent="undefined"><p>.NET 平台致力于在提升<emp>生产力、性能、安全性和可靠性</emp>的同时平衡这些优势，这正是它如此具有吸引力的原因。</p></div><p>.NET 的设计要点可以归结为：在注重生产力的安全领域和具备强大功能的不安全领域都同样高效且有效。.NET 或许是功能最丰富的托管环境，同时也能做到最低开销且不将就地与外界互操作。同时，<a href="https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/">许多特性都利用了这一点</a>，在底层操作系统和 <a href="https://devblogs.microsoft.com/dotnet/arm64-performance-improvements-in-dotnet-7/">CPU</a> 的原生性能之上构建安全的托管 API。</p><p>更进一步地说：</p><ul><li><strong>全栈生产力</strong>：包括运行时、库、语言和相关工具来共同提升开发者的使用体验。</li><li><strong>安全的代码是主要的计算模型</strong>，同时允许使用不安全代码手动额外优化。</li><li><strong>支持静态和动态代码以满足广泛且多样化的场景需求。</strong></li><li><strong>Native 代码互操作性和硬件内在函数</strong>是低开销和高精度的（即直接访问底层 API 和指令）。</li><li><strong>代码具有跨平台（操作系统和芯片架构）的可移植性</strong>，同时支持对平台进行定制和针对性优化。</li><li><strong>通过通用编程模型的<a href="https://github.com/dotnet/designs/blob/main/accepted/2020/form-factors.md">特定实现</a></strong> 以达成在不同领域（云端、客户端、游戏）之间的适配。</li><li><strong>优先使用业界标准解决方案</strong>（如 <a href="https://opentelemetry.io/">OpenTelemetry</a> 和 <a href="https://grpc.io/">gRPC</a>），而不考虑定制。</li></ul><h2 id="net-技术栈的支柱"><a class="markdownIt-Anchor" href="#net-技术栈的支柱"></a> .NET 技术栈的支柱</h2><p>运行时、库和语言是 .NET 技术栈的核心支柱。更高层的组件，像一些 .NET 工具和应用栈（如 <a href="http://ASP.NET">ASP.NET</a> Core），则构建于这些支柱之上。这些共生的支柱由一个团队（包括微软员工和开源社区）共同设计和开发，团队中的每个人在组件之间协同工作、互相启发。</p><p>C# 是一种面向对象的语言，同时 .NET 运行时为其提供了面向对象支持。C# 需要垃圾回收机制，则该运行时提供了跟踪垃圾回收器。事实上，若没有垃圾收集器 <em>(GC, garbage collection)</em>，是无法将完整的 C# 移植到其他系统的。库（以及应用栈）则将这些底层的功能塑造为让开发者直观理解的概念和对象模型，从而更自然且高效地编写 C# 算法。</p><p>C# 是一门现代、安全、通用的编程语言，涵盖了从<a href="https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/record">记录</a>这样的高级特性到<a href="https://learn.microsoft.com/dotnet/csharp/language-reference/proposals/csharp-9.0/function-pointers">函数指针</a>这样的底层功能。它也提供了静态类型检查，保证类型安全和内存安全，这些基础特性既提高了开发者的生产力，也提升了代码的安全性。此外，C# 编译器还具有可扩展性，支持插件模型，使开发者能够通过额外的诊断和编译时代码生成来扩展系统功能。</p><p>与当前的前沿编程语言互相影响产生了许多 C# 特性。例如，C# 是首个引入异步编程（<a href="https://learn.microsoft.com/dotnet/standard/parallel-programming/task-based-asynchronous-programming"><code>async</code> 和 <code>await</code></a>）的主流编程语言。同时，C# 也借鉴了其他编程语言中首次引入的一些概念，如采用了函数式编程中的<a href="https://learn.microsoft.com/dotnet/csharp/fundamentals/functional/pattern-matching">模式匹配</a>和<a href="https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/record#positional-syntax-for-property-definition">主构造函数</a>。</p><p>核心库中有上千种类型，其中许多类型都与 C# 紧密结合。比如，使用 C# 的 <a href="https://learn.microsoft.com/dotnet/csharp/language-reference/statements/iteration-statements#the-foreach-statement"><code>foreach</code></a> 语法来枚举任意集合；并提供了基于模式的优化 <em>(pattern-based optimizations)</em>，以能够简洁高效地处理诸如 <a href="https://learn.microsoft.com/dotnet/api/system.collections.generic.list-1"><code>List&lt;T&gt;</code></a> 这样的集合。资源管理则可以交由GC完成，而通过 <a href="https://learn.microsoft.com/dotnet/api/system.idisposable"><code>IDisposable</code></a> 接口和语言层面的 <a href="https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/using-statement"><code>using</code></a> 语法支持也能实现。</p><p>C# 中的<a href="https://devblogs.microsoft.com/dotnet/string-interpolation-in-c-10-and-net-6/">字符串插值</a>既富有表现力又高效，这得益于核心库中（如 <a href="https://learn.microsoft.com/dotnet/api/system.string"><code>string</code></a>、<a href="https://learn.microsoft.com/dotnet/api/system.text.stringbuilder"><code>StringBuilder</code></a>、<a href="https://learn.microsoft.com/dotnet/api/system.span-1"><code>Span&lt;T&gt;</code></a>）类型的集成与实现。<a href="https://learn.microsoft.com/dotnet/csharp/programming-guide/concepts/linq/">语言集成查询（LINQ）</a>则通过库中成百上千的序列处理方法来实现，比如 <a href="https://learn.microsoft.com/dotnet/api/system.linq.enumerable.where"><code>Where</code></a>、<a href="https://learn.microsoft.com/dotnet/api/system.linq.enumerable.select"><code>Select</code></a> 和 <a href="https://learn.microsoft.com/dotnet/api/system.linq.enumerable.groupby"><code>GroupBy</code></a>；LINQ 同时采用可扩展的设计，支持内存数据处理和远程数据源的操作。</p><p>这些例子仅仅触及了 .NET 核心库所提供的功能的冰山一角，比如从压缩到加密、正则表达式等的各类实现。一个全面的<a href="https://learn.microsoft.com/dotnet/api/system.net">网络栈</a>几乎是一个独立的领域，涵盖了从 <a href="https://learn.microsoft.com/dotnet/api/system.net.sockets"><code>Sockets</code></a> 到 <a href="https://learn.microsoft.com/dotnet/api/system.net.http">HTTP/3</a> 的全部内容。同样，库还支持处理多种格式和语言，如 <a href="https://learn.microsoft.com/dotnet/api/system.text.json">JSON</a>、<a href="https://learn.microsoft.com/dotnet/api/system.xml">XML</a> 和 <a href="https://learn.microsoft.com/dotnet/api/system.formats.tar">tar</a>。</p><p>.NET 运行时最初被称为“通用语言运行时 <em>(Common Language Runtime，CLR)</em>”。它一直以来支持多种语言，一些<a href="https://devblogs.microsoft.com/dotnet/update-to-the-dotnet-language-strategy/">由微软维护</a>（例如 C#、F#、Visual Basic、C++/CLI 和 PowerShell），另一些则由其他组织维护（例如 Cobol、<a href="https://github.com/ikvm-revived/ikvm">Java</a>、<a href="https://www.peachpie.io/">PHP</a>、<a href="https://ironpython.net/">Python</a>、<a href="https://github.com/IronScheme/IronScheme">Scheme</a>）。对运行时的许多改进都无关语言，这对所有语言都有好处。</p><p>接下来，我们一起看看这些平台特性是如何协同工作的。我们当然可以分别详细介绍每个组件，但你很快会发现，它们共同组成了 .NET 设计的核心。我们先从类型系统开始。</p><h2 id="类型系统"><a class="markdownIt-Anchor" href="#类型系统"></a> 类型系统</h2><p>.NET 的类型系统在安全、可描述性、动态类型和互操作性方面提供了极广的支持，且几乎同等程度地满足了这些需求。</p><p>首先，.NET 类型系统支持面向对象的编程范式。它包括类型、（单一基类）继承、接口（包括默认方法实现）以及虚方法，这些机制支持面向对象所有类型层次的合理行为。</p><p><a href="https://learn.microsoft.com/dotnet/csharp/fundamentals/types/generics">泛型</a>是一个普遍存在的特性，它允许类针对一个或多个类型进行特殊处理。例如，<a href="https://learn.microsoft.com/dotnet/api/system.collections.generic.list-1"><code>List&lt;T&gt;</code></a> 是一个泛型类，可以实现如 <code>List&lt;string&gt;</code> 和 <code>List&lt;int&gt;</code> 这样的实例化，避免了分别定义 <code>ListOfString</code> 和 <code>ListOfInt</code> 类的需求，也避免了像 <a href="https://learn.microsoft.com/dotnet/api/system.collections.arraylist"><code>ArrayList</code></a> 那样依赖 <code>object</code> 和类型转换的情况。泛型还能在减少大量代码的情况下支持跨类型创建有用的机制，如<a href="https://learn.microsoft.com/dotnet/standard/generics/math">泛型数学 <em>(Generic Math)</em> </a>。</p><p><a href="https://learn.microsoft.com/dotnet/csharp/delegates-overview">委托 <em>(Delegates)</em> </a>和<a href="https://learn.microsoft.com/dotnet/csharp/language-reference/operators/lambda-expressions">Lambda 表达式</a>可以将方法当作数据传递，这样就可以轻松地像是把外部代码“粘起来”集成到另一个系统的操作管理中。而且为了通用性，它们的签名通常是泛型的。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs csharp">app.MapGet(<span class="hljs-string">&quot;/Product/&#123;id&#125;&quot;</span>, <span class="hljs-keyword">async</span> (<span class="hljs-built_in">int</span> id) =&gt;<br>&#123;<br>    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">await</span> IsProductIdValid(id))<br>    &#123;<br>        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> GetProductDetails(id);<br>    &#125;<br><br>    <span class="hljs-keyword">return</span> Products.InvalidProduct;<br>&#125;);<br></code></pre></td></tr></table></figure><p>这段代码将 Lambda 表达式用于<a href="https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis/overview">ASP.NET Core Minimal APIs</a>中，它让我们可以直接通过路由系统实现端点。在新版本中，<a href="http://ASP.NET">ASP.NET</a> Core 更能得益于这个类型系统。</p><p><a href="https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/value-types">值类型</a>和<a href="https://learn.microsoft.com/dotnet/csharp/language-reference/operators/stackalloc">栈分配</a>的内存块为数据和与本机平台的交互提供了与由 .NET GC 托管的类型相比更直接、更底层的控制。.NET 中的大多数基本类型（如整型）都是值类型，用户也可以定义自己的值类型且具有类似的语义。</p><p>值类型完全受 .NET 的泛型系统支持，这意味着像 <code>List&lt;T&gt;</code> 这样的泛型类型是可以在内存里表示为扁平化的、没有额外开销的值类型集合。此外，当泛型类型替换为值类型时，.NET 会编译特化代码来避免昂贵的 GC 开销。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-built_in">byte</span> magicSequence = <span class="hljs-number">0b1000</span>_0001;<br>Span&lt;<span class="hljs-built_in">byte</span>&gt; data = <span class="hljs-keyword">stackalloc</span> <span class="hljs-built_in">byte</span>[<span class="hljs-number">128</span>];<br>DuplicateSequence(data[<span class="hljs-number">0.</span><span class="hljs-number">.4</span>], magicSequence);<br></code></pre></td></tr></table></figure><p>这段代码在栈上分配了内存。<code>Span&lt;byte&gt;</code> 是一个相比于传统的指针（<code>byte*</code>）更安全、更多功能的替代方案，它提供了长度（包括边界检查）以及方便的切片操作。</p><p><a href="https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/ref">ref</a> 类型变量是一种轻量级的编程模型，它为类型系统中的数据提供了更底层且精简的抽象，<a href="https://learn.microsoft.com/dotnet/api/system.span-1"><code>Span&lt;T&gt;</code></a> 就是其中之一。这种编程模型并非通用，而加以许多限制以确保安全性。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">internal</span> <span class="hljs-keyword">readonly</span> <span class="hljs-keyword">ref</span> T _reference;<br></code></pre></td></tr></table></figure><p><a href="https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/ref#ref-fields">这种 <code>ref</code> 的用法</a>指向了对数据的引用，而非拷贝数据本身。值类型默认是“值复制”，而 <code>ref</code> 则能“按引用复制”，这种方式可以显著提升性能。</p><h2 id="自动内存管理"><a class="markdownIt-Anchor" href="#自动内存管理"></a> 自动内存管理</h2><p>.NET 运行时通过垃圾回收机制（GC）实现对内存的自动管理。内存管理模型往往能定义一门语言，对于 .NET 的语言来说也是如此。</p><p>堆上出现的bug非常难调试，开发人员可能需要花费数周甚至数月的时间来找问题。很多语言都使用GC，通过确保对象的生命周期正确无误地被管理来做到更友好地消除这些bug。通常，GC会批量释放内存以提高效率，但这样的做法会触发一会暂停，因此GC不适合在对延迟有严格要求的场景出现，且内存占用也会更高。但GC通常能带来更好的<a href="https://devblogs.microsoft.com/dotnet/why-dotnet/">局部性内存访问</a>，一些GC还具备堆压缩能力，使其不容易出现<a href="https://en.wikipedia.org/wiki/Fragmentation_(computing)">内存碎片</a>。</p><p>.NET 有自适应的<a href="https://en.wikipedia.org/wiki/Tracing_garbage_collection">追踪型 GC</a>，旨在大多数情况下提供“无需操心”的自动操作，同时为少数极端负载场景提供配置项。多年来的投入和对各种工作负载的经验改进造就了如今的GC。</p><p><strong>指针递增分配（Bump Pointer Allocation）</strong> —— 对象通过将分配指针按所需大小递增来分配内存，而不是在离散的空闲块中寻找空间。因此，一起分配的对象通常在内存中会挨在一起。这种方法能够提高<a href="https://en.wikipedia.org/wiki/Locality_of_reference">内存局部性</a>，满足了这些通常会被一起访问的对象的性能要求。</p><p><strong>分代收集算法（Generational Collections）</strong> —— 对象的生命周期通常遵循<a href="https://en.wikipedia.org/wiki/Tracing_garbage_collection#Generational_GC_(ephemeral_GC)">分代假设</a>，即对象要么存在很久，要么很快被回收。因此，为了高效，GC大多数时候只需回收短命(ephemeral)对象占用的内存（称为短命GC, <em>ephemeral GCs</em>），而不必每次都回收整个堆内存（称为全堆GC, <em>full GCs</em>）。</p><p><strong>堆压缩（Compaction）</strong> —— 相比于分散的小块空闲空间，集中且较大的空闲空间更有用。在压缩型 <em>(compacting)</em> GC的工作过程中，存活的对象会被再次移动到一起，从而腾出较大且连续的空闲空间。这种方式比非移动型 <em>(non-moving)</em> 的GC更难实现，因为它需要更新指向这些移动对象的引用。.NET GC会动态调整，只有在认为内存回收的收益足以抵消GC的开销时才会进行压缩。这意味着很多对短命 <em>(ephemeral)</em> 对象的回收都是压缩型的。</p><p><strong>并行回收（Parallel）</strong> —— GC可以多线程运行。工作站GC在单线程上进行回收工作，而服务器GC则在多个线程上并行工作以加快回收速度。服务器GC还支持更大的分配速率，因为应用程序可以同时在不止一个堆上进行分配，因此在吞吐量方面表现出色。</p><details class="tag-plugin colorful folding" color="gray" open><summary><p>有关工作站GC和服务器GC</p></summary><div class="body"><p>可以查阅<a href="https://learn.microsoft.com/en-us/dotnet/core/runtime-config/garbage-collector#flavors-of-garbage-collection">Runtime configuration options for garbage collection - Flavors of garbage collection</a>来查看有关工作站GC (Workstation GC)和服务器GC (Server GC)的介绍和对比</p> </div></details><p><strong>并发回收（Concurrent）</strong> —— 如果GC在工作时暂停了用户线程（称为 <a href="https://en.wikipedia.org/wiki/Tracing_garbage_collection#Stop-the-world_vs._incremental_vs._concurrent">Stop-The-World</a>），实现会更简单，但这种暂停可能是无法接受的。于是 .NET 提供了<a href="https://github.com/Maoni0/mem-doc/blob/master/doc/.NETMemoryPerformanceAnalysis.md#Concurrent-GCBackground-GC">并发GC</a>来减轻这个问题。</p><p><strong>对象固定（Pinning）</strong> —— .NET GC支持固定对象，在保证对GC的影响较小的前提下，允许与native代码进行高性能、高精度的零拷贝互操作。</p><p><strong>独立GC（Standalone GC）</strong> —— 可以实现独立且不同的GC（只要通过配置指定并满足<a href="https://github.com/dotnet/runtime/blob/main/src/coreclr/gc/gcinterface.h">接口要求</a>）。这使得测试或尝试新特性变得更容易。</p><p><strong>诊断工具（Diagnostics）</strong> —— GC会提供大量关于内存和回收过程的相关信息，这些数据会被结构化以便与系统其他部分关联。例如，你可以通过捕获GC事件并将其与IO等其他事件对比，以评估<a href="https://github.com/Maoni0/mem-doc/blob/master/doc/.NETMemoryPerformanceAnalysis.md#measure-the-impact-of-factors-that-likely-affect-your-perf-metrics">GC对尾延迟的影响</a>，从而确定GC相较于其他因素的影响如何，然后将优化工作集中在真正需要的地方。</p><h2 id="安全性"><a class="markdownIt-Anchor" href="#安全性"></a> 安全性</h2><p>编程语言和环境的安全性在过去十年一直是热门话题。同时也是像 .NET 这样托管环境必需做到的事情。</p><p>.NET 的安全性体现在：</p><ul><li><a href="https://en.wikipedia.org/wiki/Type_safety">类型安全</a> — 不允许使用任意类型替代另一种类型，从而避免未定义行为。</li><li><a href="https://en.wikipedia.org/wiki/Memory_safety">内存安全</a> — 仅使用已分配的内存，例如变量要么引用一个有效对象，要么为 <code>null</code>。</li><li><a href="https://en.wikipedia.org/wiki/Thread_safety">并发与线程安全</a> — 访问共享数据不会导致未定义行为。</li></ul><details class="tag-plugin colorful folding" color="gray" open><summary><p>有关“未定义行为”</p></summary><div class="body"><p><a href="https://en.wikipedia.org/wiki/Undefined_behavior">Wikipedia - Undefined Behavior</a></p> </div></details><p>注：美国联邦政府最近发布了<a href="https://www.nsa.gov/Press-Room/News-Highlights/Article/Article/3215760/nsa-releases-guidance-on-how-to-protect-against-software-memory-safety-issues/">关于内存安全重要性的指导意见</a>。</p><p>.NET 起初就被设计为一个安全的平台。比如，它旨在支持新一代的 Web 服务器，这些服务器需要在互联网这个最具敌意的计算环境中接受不可信的输入。而如今，使用安全的语言编写 Web 程序已成为共识。</p><p>类型安全由语言和运行时共同保证。编译器会验证静态不变量 <em>(static invariants)</em>，包括不同类型的赋值操作（如将 <code>string</code> 赋值给 <code>Stream</code>，会导致编译错误）。运行时则验证动态不变量 <em>(dynamic invariants)</em>，例如不同类型之间的转换会抛出 <a href="https://learn.microsoft.com/dotnet/api/system.invalidcastexception">InvalidCastException</a>。</p><p>内存安全主要通过代码生成器（如 JIT）和垃圾回收器的协作实现。变量要么引用有效的对象，要么为 <code>null</code>，或者已经超出作用域。内存默认自动初始化，确保新对象不会使用未经初始化的内存。边界检查会确保访问无效索引的元素不会读取未定义的内存（通常由越界访问引起），而是抛出 <a href="https://learn.microsoft.com/dotnet/api/system.indexoutofrangeexception">IndexOutOfRangeException</a>。</p><p>对 <code>null</code> 的处理是内存安全的一个具体表现。C# 的<a href="https://learn.microsoft.com/dotnet/csharp/nullable-references">可空引用类型</a>是一个语言和编译器共同实现的特性，它能静态地识别未安全处理 <code>null</code> 的代码。具体来说，如果你解引用一个可能为 <code>null</code> 的变量，编译器会发出警告。你还可以禁止 <code>null</code> 赋值，这样当你可能赋一个 <code>null</code> 值给变量时，编译器也会发出警告。运行时有相应的动态验证功能，防止访问 <code>null</code> 引用，若发生则抛出 <a href="https://learn.microsoft.com/dotnet/api/system.nullreferenceexception">NullReferenceException</a>。</p><p>这一特性依赖于库中的<a href="https://learn.microsoft.com/dotnet/csharp/language-reference/attributes/nullable-analysis">可空属性</a>。它还依赖于这些属性在库和应用栈中全面的应用，以便用户代码可以通过静态分析工具获得准确的结果。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-built_in">string</span>? SomeMethod() =&gt; <span class="hljs-literal">null</span>;<br><span class="hljs-built_in">string</span> <span class="hljs-keyword">value</span> = SomeMethod() ?? <span class="hljs-string">&quot;default string&quot;</span>;<br></code></pre></td></tr></table></figure><p>由于使用了 <code>??</code>（<a href="https://learn.microsoft.com/dotnet/csharp/language-reference/operators/null-coalescing-operator">Null 合并操作符</a>）明确声明和处理了 <code>null</code>，这段代码被 C# 编译器认为是空安全的。变量 <code>value</code> 符合其声明，始终不为 <code>null</code>。</p><p>.NET 并没有内置的并发安全机制。开发者需要遵循特定的模式和约定来避免未定义行为。此外，.NET 生态系统中还有分析器和其他工具，可以帮助识别并发导致的问题。核心库也包含了许多线程安全的类型和方法，例如支持任意数量的并发读写而不会导致数据结构损坏的<a href="https://learn.microsoft.com/dotnet/api/system.collections.concurrent">并发集合</a>。</p><p>.NET 允许安全和<a href="https://learn.microsoft.com/dotnet/csharp/language-reference/unsafe-code">不安全代码</a>。安全代码是默认保证安全性的，而开发者必须选择性地使用不安全代码。不安全代码通常用于与底层平台、硬件交互，或用于手动性能优化。</p><p><a href="https://en.wikipedia.org/wiki/Sandbox_(computer_security)">沙箱</a>是一种特殊的安全机制，它隔离并限制组件之间的访问。我们依赖标准的隔离技术，如进程（和 CGroups）、虚拟机以及 WebAssembly，它们各自具有不同的特性。</p><h2 id="错误处理"><a class="markdownIt-Anchor" href="#错误处理"></a> 错误处理</h2><p>.NET 中主要的错误处理模型是异常处理。异常(Exception)好在不需要在每个方法中处理，也不需要在方法签名中体现。</p><p>比如：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">try</span><br>&#123;<br>    <span class="hljs-keyword">var</span> lines = <span class="hljs-keyword">await</span> File.ReadAllLinesAsync(<span class="hljs-keyword">file</span>);<br>    Console.WriteLine(<span class="hljs-string">$&quot;The <span class="hljs-subst">&#123;<span class="hljs-keyword">file</span>&#125;</span> has <span class="hljs-subst">&#123;lines.Length&#125;</span> lines.&quot;</span>);<br>&#125;<br><span class="hljs-keyword">catch</span> (Exception e) <span class="hljs-keyword">when</span> (e <span class="hljs-keyword">is</span> FileNotFoundException <span class="hljs-keyword">or</span> DirectoryNotFoundException)<br>&#123;<br>    Console.WriteLine(<span class="hljs-string">$&quot;<span class="hljs-subst">&#123;<span class="hljs-keyword">file</span>&#125;</span> doesn&#x27;t exist.&quot;</span>);<br>&#125;<br></code></pre></td></tr></table></figure><p>可以特意处理预期内的异常以避免应用程序崩溃。相比于出现未定义行为，一个崩溃的应用程序更能被诊断且可靠。</p><p>异常会在错误发生的位置被抛出，并自动收集关于程序状态的额外诊断信息，这些信息可用于交互式调试、应用程序可观测性 <em>(Application Observability)</em> 以及事后调试。这些诊断方法都依赖于收集到的丰富的错误信息和应用状态。</p><p>异常处理应少用，一部分原因是处理异常的性能开销相对较高。尽管有时会将其用于控制流，但这并非其设计初衷。</p><p>异常也可用于取消操作 <em>(Cancellation)</em>。它们能够在收到取消请求后，高效地停止执行并展开调用栈，从而中止正在进行的工作。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">try</span> <br>&#123; <br>    <span class="hljs-keyword">await</span> source.CopyToAsync(destination, cancellationToken); <br>&#125; <br><span class="hljs-keyword">catch</span> (OperationCanceledException) <br>&#123; <br>    Console.WriteLine(<span class="hljs-string">&quot;Operation was canceled&quot;</span>); <br>&#125;<br></code></pre></td></tr></table></figure><p>由于异常处理的性能开销过高，.NET 提供了另一种错误处理的设计模式。例如，<a href="https://learn.microsoft.com/dotnet/api/system.int32.tryparse"><code>int.TryParse</code></a> 返回一个 <code>bool</code>，并通过 <code>out</code> 参数返回解析成功的整数值。同样对于 <a href="https://learn.microsoft.com/dotnet/api/system.collections.generic.dictionary-2.trygetvalue"><code>Dictionary&lt;TKey, TValue&gt;.TryGetValue</code></a>，它会在成功的情况下通过 <code>out</code> 参数返回有效的 <code>TValue</code> 类型的值。</p><p>错误处理以及更广泛的诊断功能由底层的运行时 API、<a href="https://opentelemetry.io/docs/instrumentation/net/">高级库</a>和<a href="https://learn.microsoft.com/dotnet/core/diagnostics/#net-core-diagnostic-global-tools">工具</a>来实现。这些功能已被设计用于支持如容器等新型部署选项。例如，<a href="https://learn.microsoft.com/dotnet/core/diagnostics/dotnet-monitor">dotnet-monitor</a>可以通过内置的面向诊断的 Web 服务器，将运行时数据从应用程序导出到监听器。</p><h2 id="并发"><a class="markdownIt-Anchor" href="#并发"></a> 并发</h2><p>并发处理在几乎所有场景中都是很必要的：无论是应用程序在后台处理任务的同时保持 UI 的响应、处理成千上万的并发请求服务、同时响应大量设备，还是高性能机器并行处理计算密集型操作。操作系统提供了线程并将这些线程调度在机器的可用 CPU 核心上执行，这也赋予了机器并发地独立处理多个指令流的能力。操作系统还支持 I/O 操作，也提供了可扩展地执行大量 I/O 操作的机制，使得在任意时刻都“在进行”多个 I/O 操作。编程语言和框架则在这一核心支持之上提供了各种层次的抽象。</p><p>.NET 在多个抽象层上提供了并发和并行处理的支持，这些支持既通过库实现，又深度集成在 C# 语言中。<a href="https://learn.microsoft.com/dotnet/api/system.threading.thread"><code>Thread</code></a> 类位于最底层，即代表一个线程，允许开发者创建新线程并在该线程上运行代码。<a href="https://learn.microsoft.com/dotnet/api/system.threading.threadpool"><code>ThreadPool</code></a> 则基于线程，使开发者能够以工作项的形式异步调度任务到线程池中，并由运行时负责线程的管理（包括线程池中线程的增加和移除，以及工作项的分配）。</p><p>可以通过多种方式创建 <a href="https://learn.microsoft.com/dotnet/api/system.threading.tasks.task"><code>Task</code></a> 来表示异步操作。例如，<code>Task.Run</code> 允许将一个委托调度到 <code>ThreadPool</code> 中运行，并返回一个 <code>Task</code> 来表示该操作的最终完成情况；而 <code>Socket.ReceiveAsync</code> 则返回一个 <code>Task&lt;int&gt;</code>（或 <code>ValueTask&lt;int&gt;</code>），表示从 <code>Socket</code> 异步 I/O 操作读取的待处理数据的最终完成情况。</p><p>.NET 提供了丰富的同步原语 <em>(Synchronization Primitives)</em>，用于同步和协调线程以及异步操作。此外，还有许多高级 API 简化了常见的并发模式实现。如<a href="https://learn.microsoft.com/dotnet/api/system.threading.tasks.parallel"><code>Parallel.ForEach</code></a> 和 <code>Parallel.ForEachAsync</code> 使得并行处理数据序列中的所有元素变得更加容易。</p><p>异步编程也是 C# 语言的一等 <em>(first-class)</em> 特性，提供了 <code>async</code> 和 <code>await</code> 关键字，使编写和组合异步操作变得简便，同时仍然能够充分利用语言提供的各种控制流结构的优势。这些关键字让开发者能够以同步代码的方式编写异步逻辑，提高了代码的可读性和可维护性。</p><h2 id="反射"><a class="markdownIt-Anchor" href="#反射"></a> 反射</h2><p><a href="https://learn.microsoft.com/dotnet/framework/reflection-and-codedom/reflection">反射</a>是一种“将程序视为数据”的编程范式，允许程序的一部分动态地查询和/或调用另一部分，包括<a href="https://learn.microsoft.com/dotnet/api/system.reflection">程序集、类型和成员</a>。它在延迟绑定 <em>(late binding)</em> 的编程模型和工具中尤其有用。</p><p>以下代码使用了反射来查找并调用类型：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">foreach</span> (<span class="hljs-function">Type type <span class="hljs-keyword">in</span> <span class="hljs-title">typeof</span>(<span class="hljs-params">Program</span>).Assembly.DefinedTypes)</span><br>&#123;<br>    <span class="hljs-keyword">if</span> (type.IsAssignableTo(<span class="hljs-keyword">typeof</span>(IStory)) &amp;&amp;<br>        !type.IsInterface)<br>    &#123;<br>        IStory? story = (IStory?)Activator.CreateInstance(type);<br>        <span class="hljs-keyword">if</span> (story <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>)<br>        &#123;<br>            <span class="hljs-keyword">var</span> text = story.TellMeAStory();<br>            Console.WriteLine(text);<br>        &#125;<br>    &#125;<br>&#125;<br><br><span class="hljs-keyword">interface</span> <span class="hljs-title">IStory</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-built_in">string</span> <span class="hljs-title">TellMeAStory</span>()</span>;<br>&#125;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title">BedTimeStore</span> : <span class="hljs-title">IStory</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> <span class="hljs-title">TellMeAStory</span>()</span> =&gt; <span class="hljs-string">&quot;Once upon a time, there was an orphan learning magic ...&quot;</span>;<br>&#125;<br><br><span class="hljs-keyword">class</span> <span class="hljs-title">HorrorStory</span> : <span class="hljs-title">IStory</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> <span class="hljs-title">TellMeAStory</span>()</span> =&gt; <span class="hljs-string">&quot;On a dark and stormy night, I heard a strange voice in the cellar ...&quot;</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>这段代码动态枚举了一个程序集内所有实现特定接口的类型，并实例化这些类型的对象，然后通过该接口调用这些对象的方法。实际上，这段代码也可以静态编写，因为它只是查询当前引用的程序集中的类型。不过，如果采用静态的写法，代码需要接收一个包含所有实例的集合，比如 <code>List&lt;IStory&gt;</code>。这种延迟绑定的方法更适用于如加载来自任意程序集的插件的场景。反射常常被用到当程序集和类型事先未知时的情况。</p><p>反射可能是 .NET 中最动态的机制。它旨在让开发者创建自定义的二进制代码加载器和方法调度器，其语义可以与（由运行时定义的）静态代码相匹配或有所不同。反射提供了一个<a href="https://learn.microsoft.com/dotnet/api/system.reflection">丰富的对象模型</a>，对于特殊使用场景来说易于使用，但当场景变得更复杂时，则需要更深入地理解 .NET 的类型系统。</p><p>此外，反射还支持一种独立的模式，即<a href="https://learn.microsoft.com/dotnet/framework/reflection-and-codedom/emitting-dynamic-methods-and-assemblies">生成的 IL 字节码可以在运行时被即时编译（JIT）</a>，有时用于编写特定算法以替代通用算法。在序列化器或对象关系映射器中，反射常被用来在对象模型和其他细节确定后执行相关操作。</p><h2 id="编译后的二进制格式"><a class="markdownIt-Anchor" href="#编译后的二进制格式"></a> 编译后的二进制格式</h2><p>应用程序和库被编译成采用<a href="https://en.wikipedia.org/wiki/COFF">PE/COFF 格式</a>的<a href="https://github.com/dotnet/runtime/blob/main/docs/project/dotnet-standards.md">标准化</a>跨平台<a href="https://en.wikipedia.org/wiki/Common_Intermediate_Language">字节码</a>。二进制分发使得应用程序能够扩展到越来越多的项目。每个库都包含一个导入和导出类型的数据库，称为<a href="https://www.nuget.org/packages/System.Reflection.Metadata">元数据</a>，这在开发和运行应用程序时都起着重要作用。</p><p>编译后的二进制文件包括两个主要部分：</p><ul><li>二进制字节码 — 这种紧凑且规范的格式无需在经过语言编译器（如 C#）抽象后编译并解析源代码。</li><li>元数据 — 描述导入和导出的类型，包括特定方法在字节码中的位置。</li></ul><p>在开发过程中，工具可以高效地读取元数据，以确定在给定库中公开的类型集合，以及哪些类型实现了特定接口等。这一过程使编译速度更快，并使 IDE 和其他工具能够在特定上下文中准确地显示类型和成员列表。</p><p>在运行时，元数据允许库和方法体被延迟加载。反射（稍后讨论）是用于元数据和 IL 的运行时 API。但对于工具来说，还有其他更合适的 API。</p><p>IL 的格式依旧保持向后兼容。最新版本的 .NET 仍然可以加载和执行由 .NET Framework 1.0 编译器生成的二进制文件。</p><p>共享库通常用 <a href="https://www.nuget.org/">NuGet</a> 进行分发。NuGet 包默认情况下可以在任何操作系统和架构上运行，但也可以指定环境以提供特定行为。</p><h2 id="代码生成"><a class="markdownIt-Anchor" href="#代码生成"></a> 代码生成</h2><p>.NET 的字节码并不是能让机器直接执行的格式，而需要通过某种形式的代码生成器将其转化为可执行代码。这可以通过提前编译 <em>(ahead-of-time, AOT)</em>、即时编译 <em>(just-in-time, JIT)</em>、解释执行或转译来实现，这些方法在各种场景中都在使用。</p><p>.NET 最为人熟知的是即时编译（JIT）。JIT 得名“即时”是因为它在应用程序运行时按需地将方法（及成员）编译成 native 代码。例如，一个程序在运行时可能只会调用某个类型的几个方法中的一个。JIT 也可以利用运行时获得的信息来优化，比如已初始化的只读静态变量的值或程序运行时机器的 CPU 型号，并且可以多次编译同一个方法，以便借鉴之前编译的经验在每次编译时针对不同的目标优化。</p><details class="tag-plugin colorful folding" color="gray" open><summary><p>按配置优化</p></summary><div class="body"><p>按配置优化 <em>(Profile-Guided Optimization, PGO)</em> 是指 JIT 编译器根据最常使用的类型和代码路径生成优化后的代码。 动态 PGO 与分层编译携手合作，根据第 0 层期间实施的其他检测进一步优化代码。</p> <p><a href="https://learn.microsoft.com/zh-cn/dotnet/core/runtime-config/compilation#profile-guided-optimization">来源：用于编译的运行时配置选项</a></p> </div></details><p>JIT 会根据特定的操作系统和芯片架构生成代码。 .NET 具有支持 Arm64 和 x64 指令集以及 Linux、macOS 和 Windows 操作系统的 JIT 实现。作为 .NET 开发者，你无需担心 CPU 指令集和操作系统调用之间的差异。JIT 会负责生成 CPU 所需的代码。它还知道如何为各种 CPU 生成高效的代码，操作系统和 CPU 厂商通常会协助我们实现这一点。</p><p>AOT 与 JIT 类似，但会在程序运行前生成 native 代码。开发者选择这种方式是因为它可以显著提高程序启动速度，省去了 JIT 所需的启动工作。AOT 的应用程序本质上是构建在特定的操作系统和架构的，这意味着需要额外的步骤才能使应用程序在多种环境中运行。例如，如果你希望支持 Linux 和 Windows 以及 Arm64 和 x64，那么你需要构建四个版本才能做到。AOT 生成的代码也可以提供有价值的优化，但通常不如 JIT 那么多。</p><details class="tag-plugin colorful folding" color="gray" open><summary><p>有关“本机 AOT 部署”</p></summary><div class="body"><p>Native AOT 应用启动更快且内存占用更小，且可以在没有安装 .NET 运行时的机器上运行。</p> <p><a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?tabs=windows%2Cnet8">来源：Native AOT deployment</a></p> </div></details><p>我们将在后续的文章中讨论解释执行和转译，它们在我们的生态系统中也扮演着关键角色。</p><p>代码生成器优化之一是内在函数 <em>(intrinsics)</em>。<a href="https://github.com/dotnet/designs/blob/main/accepted/2018/platform-intrinsics.md">硬件内在函数</a>可以将 <a href="https://learn.microsoft.com/dotnet/api/system.numerics">.NET API</a> 直接转换为 CPU 指令。这在 .NET 库中广泛用于执行 <a href="https://en.wikipedia.org/wiki/Single_instruction,_multiple_data">SIMD</a>。</p><h2 id="互操作性"><a class="markdownIt-Anchor" href="#互操作性"></a> 互操作性</h2><p>.NET 设计了与本机库进行低开销互操作的特性。.NET 程序和库可以无缝调用底层操作系统的 API 或庞大的 C/C++ 生态。现代的 .NET 运行时专注于提供底层的互操作构建，例如通过函数指针调用本机库的方法、将托管方法暴露为<a href="https://learn.microsoft.com/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute">非托管回调</a>或<a href="https://learn.microsoft.com/dotnet/api/system.runtime.interopservices.idynamicinterfacecastable">自定义的接口转换</a>。.NET 也在这一领域不断发展。.NET 7 发布了 AOT 友好的<a href="https://learn.microsoft.com/dotnet/standard/native-interop/pinvoke-source-generation">源生成解决方案</a>，进一步降低了开销。</p><p>以下代码展示了 .NET 7 引入的 <code>LibraryImport</code> 源生成器与高效的 C# 函数指针（该源生成器是在 .NET 诞生以来就存在的 <code>DllImport</code> 之上提供的支持）。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-comment">// 使用函数指针避免分配委托</span><br><span class="hljs-comment">// 相当于 C 语言中的 `void (*fptr)(int) = &amp;RegisterCallback;`</span><br><span class="hljs-built_in">delegate</span>* <span class="hljs-keyword">unmanaged</span>&lt;<span class="hljs-built_in">int</span>, <span class="hljs-keyword">void</span>&gt; fptr = &amp;RegisterCallback;<br>RegisterCallback(fptr);<br><br>[<span class="hljs-meta">UnmanagedCallersOnly</span>]<br><span class="hljs-function"><span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Callback</span>(<span class="hljs-params"><span class="hljs-built_in">int</span> a</span>)</span> =&gt; Console.WriteLine(<span class="hljs-string">$&quot;Callback:  <span class="hljs-subst">&#123;a&#125;</span>&quot;</span>);<br><br>[<span class="hljs-meta">LibraryImport(<span class="hljs-string">&quot;...&quot;</span>, EntryPoint = <span class="hljs-string">&quot;RegisterCallback&quot;</span>)</span>]<br><span class="hljs-function"><span class="hljs-keyword">static</span> <span class="hljs-keyword">partial</span> <span class="hljs-keyword">void</span> <span class="hljs-title">RegisterCallback</span>(<span class="hljs-params"><span class="hljs-built_in">delegate</span>* <span class="hljs-keyword">unmanaged</span>&lt;<span class="hljs-built_in">int</span>, <span class="hljs-keyword">void</span>&gt; fptr</span>)</span>;<br></code></pre></td></tr></table></figure><p>独立的包通过这些底层构建模块，提供更高抽象层的特定领域互操作方案，例如 <a href="https://github.com/dotnet/ClangSharp">ClangSharp</a>、<a href="https://github.com/xamarin/xamarin-macios">Xamarin.iOS &amp; Xamarin.Mac</a>、<a href="https://github.com/microsoft/CsWinRT">CsWinRT</a>、<a href="https://github.com/microsoft/CsWin32">CsWin32</a> 以及 <a href="https://github.com/AaronRobinsonMSFT/DNNE">DNNE</a>。</p><p>这些新特性的出现并不意味着内置的互操作方案，如内置的运行时托管/非托管封送 <em>(Marshalling)</em> 或 Windows COM 互操作不再有用 —— 我们知道这些功能仍然非常有用，且人们已经依赖于它们。这些历史上内置于运行时的功能将继续在 .NET 运行时中得到支持。然而，这些仅停留于向后兼容，未来没有进一步的发展计划。以后将集中在互操作构建模块以及它们所支持的特定领域方案上。</p><h2 id="二进制发行版"><a class="markdownIt-Anchor" href="#二进制发行版"></a> 二进制发行版</h2><p>微软的 .NET 团队维护了<a href="https://github.com/dotnet/core/blob/main/os-lifecycle-policy.md">多个二进制发行版</a>，最近又支持了 Android、iOS 和 <a href="https://learn.microsoft.com/aspnet/core/blazor/hosting-models#blazor-webassembly">Web Assembly</a>。团队采用多种技术针对每个平台进行代码库的优化。平台的大部分使用 C# 编写，这使得移植工作可以集中在相对较少的组件上。</p><p>社区主要聚焦于 Linux，继而维护了<a href="https://github.com/dotnet/core/blob/main/linux.md">另一组发行版</a>。例如，.NET 已被包含在 <a href="https://pkgs.alpinelinux.org/packages?name=dotnet*">Alpine Linux</a>、<a href="https://packages.fedoraproject.org/search?query=dotnet">Fedora</a>、<a href="http://redhatloves.net/">Red Hat Enterprise Linux</a> 和 <a href="https://ubuntu.com/blog/install-dotnet-on-ubuntu">Ubuntu</a> 中。</p><p>社区还将 .NET 扩展到了其他平台。比如<a href="https://developer.samsung.com/tizen/About-Tizen.NET/Tizen.NET.html">三星为其基于 Arm 的 Tizen 平台移植了 .NET</a>。<a href="http://redhatloves.net/">Red Hat</a> 和 <a href="https://community.ibm.com/community/user/ibmz-and-linuxone/blogs/elizabeth-k-joseph1/2021/11/10/net-6-comes-to-ibm-z-and-linuxone">IBM 将 .NET 移植到了 LinuxONE/s390x</a>。<a href="https://www.loongson.cn/">龙芯科技（Loongson Technology）</a>将 <a href="https://github.com/dotnet/runtime/issues/59561">.NET 移植到了 LoongArch</a>。我们期待有新的合作伙伴将 .NET 移植到更多环境中。</p><p>Unity Technologies <a href="https://blog.unity.com/technology/unity-and-net-whats-next">已启动一项为期多年的计划</a>以迁移至现代的 .NET 运行时。</p><p><a href="https://github.com/dotnet/dotnet">.NET 开源项目</a>的维护和结构设计旨在让个人、公司及其他组织能够在传统的<a href="https://www.redhat.com/en/blog/what-open-source-upstream">上游模式 <em>(upstream model)</em> </a>中协同合作。微软作为平台的管理者，负责治理项目并提供基础设施（如 CI pipelines）。微软团队与各组织合作，帮助他们成功使用和/或<a href="https://github.com/dotnet/runtime/blob/main/docs/design/coreclr/botr/guide-for-porting.md">移植 .NET</a>。该项目拥有广泛的上游政策，包括接受针对特定发行版的更改。</p><p>一个重点是 <a href="https://github.com/dotnet/source-build">从源码构建的项目</a>，<a href="https://github.com/dotnet/core/blob/main/linux.md">多个组织</a>使用它根据典型的发行版规则（例如 <a href="https://devblogs.microsoft.com/dotnet/dotnet-6-is-now-in-ubuntu-2204/">Canonical (Ubuntu)</a>）构建 .NET。最近，这一点随着<a href="https://github.com/dotnet/dotnet">虚拟单仓库 <em>(Virtual Mono Repo，VMR)</em> </a>的加入而扩展：由于 .NET 项目由许多仓库组成，这有助于提高 .NET 开发者的效率，但也使得构建完整的运行时变得更加困难。</p><h2 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h2><p>加上最近发布的 .NET 7，我们已经体验了多个现代的 .NET 版本。我们认为，总结自 .NET Core 1.0 以来在底层所做的努力是非常有用的。我们铭记 .NET 的初心，同时向着一个全新的平台走出一条新路，为开发者提供了更多的价值。</p><p>让我们回到最初的地方。 .NET 的价值体现在：生产力、性能、安全性和可靠性。我们坚信，当不同的语言平台提供不同的路子时，开发者会获得最大的收益。作为一个团队，我们致力于为 .NET 开发者提供高生产力，同时构建在性能、安全性和可靠性方面领先的平台。</p>]]>
    </content>
    <id>https://ziling.moe/2024/dotnet-devblog-why-dotnet/</id>
    <link href="https://ziling.moe/2024/dotnet-devblog-why-dotnet/"/>
    <published>2024-11-21T11:40:00.000Z</published>
    <summary>
      <![CDATA[<p>本文翻译自 <a href="https://devblogs.microsoft.com/dotnet/why-dotnet">What is .NET, and why should you choose it?</a></p>]]>
    </summary>
    <title>什么是 .NET？ 你为什么应该选择它？</title>
    <updated>2024-11-21T11:40:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Artemis Li</name>
    </author>
    <category term=".NET Learning" scheme="https://ziling.moe/categories/NET-Learning/"/>
    <category term=".NET" scheme="https://ziling.moe/tags/NET/"/>
    <category term="数据结构" scheme="https://ziling.moe/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    <content>
      <![CDATA[<p>为了解决哈希表中可能出现的数据冲突，需要对哈希表的数据结构和哈希算法进行改进。</p><span id="more"></span><h2 id="解决哈希冲突的策略"><a class="markdownIt-Anchor" href="#解决哈希冲突的策略"></a> 解决哈希冲突的策略</h2><p>当不同的键（Key）通过哈希函数计算出相同的索引时，就发生了<emp>哈希冲突 (Hash Collision)</emp>。<br />常见的解决策略主要有两种：<strong>开放寻址法</strong>和<strong>链式地址法</strong>。</p><h3 id="开放寻址法-open-addressing"><a class="markdownIt-Anchor" href="#开放寻址法-open-addressing"></a> 开放寻址法 (Open Addressing)</h3><p>开放寻址的核心思想是：当目标位置 <code>bucket[i]</code> 被占用时，按照某种规则寻找下一个空闲位置 <code>bucket[j]</code>。所有数据都存储在同一个哈希表数组中。</p><p>最简单的探测方式是<emp>线性探测 (Linear Probing)</emp>：</p><ul><li><strong>存入</strong>：如果位置 <code>i</code> 被占用，则检查 <code>i+1</code>，直到找到空位。</li><li><strong>查找</strong>：计算位置 <code>i</code>，如果该位置不是目标 Key，则检查 <code>i+1</code>，直到找到目标或遇到空位（说明不存在）。</li></ul><p>这种方式的优点是内存数据连续，对 CPU 缓存友好。但缺点也很明显：</p><ul><li><strong>聚集现象 (Clustering)</strong>：冲突的数据容易连成一片，导致后续插入和查找效率急剧下降。</li><li><strong>删除困难</strong>：直接删除元素会“断链”，导致后续元素无法被查找。通常需要使用特殊的标记（如 <code>Deleted</code>）来代替物理删除。</li></ul><h3 id="链式地址法-chaining"><a class="markdownIt-Anchor" href="#链式地址法-chaining"></a> 链式地址法 (Chaining)</h3><p>链式地址法将哈希表的每个 bucket 视为一个链表的头节点。当发生冲突时，将新元素添加到该 bucket 对应的链表中。</p><ul><li><strong>存入</strong>：计算哈希值定位 bucket，遍历链表检查 Key 是否已存在。若不存在，将新节点追加到链表末尾（或头部）。</li><li><strong>查找</strong>：定位 bucket，遍历链表寻找匹配的 Key。</li></ul><p>这种方式的优点是：</p><ul><li><strong>容忍度高</strong>：不像开放寻址法那样受限于数组长度，理论上链表可以无限延伸（只要内存足够）。</li><li><strong>删除简单</strong>：只需从链表中移除节点即可。</li></ul><p>缺点则是链表节点需要额外的内存存储指针，且分散的内存节点对 CPU 缓存不友好。</p><h2 id="net-的哈希实现"><a class="markdownIt-Anchor" href="#net-的哈希实现"></a> .NET 的哈希实现</h2><h3 id="优秀的哈希函数"><a class="markdownIt-Anchor" href="#优秀的哈希函数"></a> 优秀的哈希函数</h3><p>哈希算法的优化目标是将键值对<strong>均匀地分布</strong>到哈希表中，避免为了减少冲突而导致数据在某些 buckets 中堆积。</p><p>在 .NET 中，<code>Dictionary&lt;TKey, TValue&gt;</code> 的核心逻辑依赖于 <code>GetHashCode()</code> 方法。当插入元素时，它会计算 Key 的哈希值，并通过取模运算（或者位运算）映射到 <code>buckets</code> 数组的索引。</p><p>参考源代码 <a href="https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs,526">Dictionary.cs</a>：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-comment">// 1. 获取哈希值</span><br><span class="hljs-built_in">uint</span> hashCode = (<span class="hljs-built_in">uint</span>)((<span class="hljs-keyword">typeof</span>(TKey).IsValueType &amp;&amp; comparer == <span class="hljs-literal">null</span>) <br>    ? key.GetHashCode() <br>    : comparer!.GetHashCode(key));<br><br><span class="hljs-comment">// 2. 定位 bucket（.NET Core 优化了取模运算）</span><br><span class="hljs-keyword">ref</span> <span class="hljs-built_in">int</span> bucket = <span class="hljs-function"><span class="hljs-keyword">ref</span> <span class="hljs-title">GetBucket</span>(<span class="hljs-params">hashCode</span>)</span>;<br></code></pre></td></tr></table></figure><h3 id="冲突处理机制"><a class="markdownIt-Anchor" href="#冲突处理机制"></a> 冲突处理机制</h3><p>值得注意的是，早期版本的 .NET Framework 和 .NET Core 中，<code>Dictionary&lt;TKey, TValue&gt;</code> 仅使用<strong>链式地址法</strong>（通过 <code>next</code> 数组模拟链表）来解决冲突。</p><p>与 Java 的 <code>HashMap</code> 不同（Java 8+ 会在链表过长时转换为红黑树），<strong>.NET 的标准 <code>Dictionary</code> 不会将链表转换为红黑树</strong>。如果发生大量哈希冲突（例如哈希洪水攻击 Hash Flooding），性能会退化为 <code>O(n)</code>。</p><p>不过，为了应对这种攻击，.NET 在检测到频繁冲突时，可能会动态切换哈希种子（Seed）并重组哈希表。</p>]]>
    </content>
    <id>https://ziling.moe/2024/dotnet-data-structure-hash-conflict/</id>
    <link href="https://ziling.moe/2024/dotnet-data-structure-hash-conflict/"/>
    <published>2024-09-19T03:45:30.000Z</published>
    <summary>
      <![CDATA[<p>为了解决哈希表中可能出现的数据冲突，需要对哈希表的数据结构和哈希算法进行改进。</p>]]>
    </summary>
    <title>.NET 数据结构：哈希冲突与算法</title>
    <updated>2024-09-19T03:45:30.000Z</updated>
  </entry>
</feed>
