<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>SamHou's Blog</title>
    <link>https://blog.samhou.moe/</link>
    <description>Sleep for a better life.</description>
    <language>zh-CN</language>
    <copyright>All rights reserved 2026, Sam Hou</copyright>
    <lastBuildDate>Tue, 10 Mar 2026 14:04:13 GMT</lastBuildDate>
    <generator>Hexo</generator>
    <atom:link href="https://blog.samhou.moe/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>让你验证请求来自 CF —— Authenticated Origin Pulls 攻略</title>
      <link>https://blog.samhou.moe/cf-authenticated-origin-pulls/</link>
      <description>本文详细介绍了如何通过 &quot;Authenticated Origin Pulls&quot; 功能验证请求是否来自 Cloudflare 边缘节点，从而提升源站安全性。通过启用 mTLS 技术，使用 Cloudflare 提供的客户端证书配置 Nginx 实现验证，有效防止源站 IP 泄露与绕过攻击。</description>
      <author>Sam Hou</author>
      <category domain="https://blog.samhou.moe/categories/safety-encryption/">安全与加密</category>
      <category domain="https://blog.samhou.moe/tags/cloudflare/">cloudflare</category>
      <category domain="https://blog.samhou.moe/tags/cdn/">cdn</category>
      <category domain="https://blog.samhou.moe/tags/waf/">waf</category>
      <category domain="https://blog.samhou.moe/tags/nginx/">nginx</category>
      <category domain="https://blog.samhou.moe/tags/ssl/">ssl</category>
      <category domain="https://blog.samhou.moe/tags/mtls/">mtls</category>
      <pubDate>Tue, 10 Mar 2026 14:04:13 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>套 Cloudflare，应该是各位站长的标准操作了吧。</p><div class="note warning flat"><p><strong>注意 SSL 配置</strong></p><p>本文的前提是 Cloudflare 开启 SSL 严格&#x2F;严格（完全）。否则无效。</p></div><h2 id="泄露-IP-和域名"><a href="#泄露-IP-和域名" class="headerlink" title="泄露 IP 和域名"></a>泄露 IP 和域名</h2><p>当你建了个站，把你的域名解析到服务器，打开小黄云，你有没有想过——你怎么知道请求是不是 CF 传递过来的呢？</p><p>众所周知，你的 Nginx 反向代理会根据请求的 Host 头判断目标服务，比如下面的这个 server 配置就来自我的某台服务器：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><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 nginx"><span class="hljs-section">server</span> &#123;<br>    <span class="hljs-attribute">listen</span> <span class="hljs-number">443</span> ssl;<br>    <span class="hljs-attribute">listen</span> [::]:<span class="hljs-number">443</span> ssl;<br>    ... <span class="hljs-comment"># ssl</span><br>    <span class="hljs-attribute">server_name</span> status.samhou.top; <span class="hljs-comment"># Host</span><br>    <span class="hljs-section">location</span> / &#123;<br>        <span class="hljs-attribute">proxy_pass</span> http://uptime;<br>        ...<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>也就是说，只要有人知道了你的服务域名+你的服务器 IP，就可以给你的源站发送请求，绕过 cloudflare 直接获取内容。</p><p>这就麻烦了——你可能配了一堆 WAF，又是速率限制又是区域锁定，结果源站泄露，直接变得跟摆设一样。</p><p>不信？你试试下面的 curl 命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -k https://ip -H <span class="hljs-string">&quot;Host: example.com&quot;</span><br></code></pre></td></tr></table></figure><p>替换成你的源站 IP 和你的域名试试执行。</p><p><img src="https://img.samhou.top/1773150190381.webp" alt="html 全都出来了"></p><p>那怎么让源站验证你的请求确实来自 Cloudflare 的边缘节点呢？别急，Cloudflare 给你藏了一个非常隐秘的选项。</p><h2 id="经过身份验证的源服务器拉取"><a href="#经过身份验证的源服务器拉取" class="headerlink" title="经过身份验证的源服务器拉取"></a>经过身份验证的源服务器拉取</h2><p>先选中域名，然后导航到 SSL&#x2F;TLS - 源服务器 - 经过身份验证的源服务器拉取，打开这个：</p><p><img src="https://img.samhou.top/1773150463554.webp" alt="经过验证的源服务器拉取"></p><p>这个选项会让 Cloudflare 向源服务器发送自己的证书。</p><p>什么意思呢？</p><p>大家都知道，源站有 SSL 证书来验证自己的身份（SSL 完全严格模式），那么客户端当然也可以有证书来验证自己的身份（这称之为 mTLS 技术）。在我们的情景下，源站是服务端，客户端是 Cloudflare 的边缘节点。</p><p>打开这个选项后，边缘节点将向源站发送 Cloudflare 的证书。源站验证证书，确认来自 Cloudflare 之后，放行。否则，直接拒绝访问。</p><p>我知道大家都用 Nginx，没错，它可以在源站充当身份认证的功能，只需两步——</p><p>首先，下载 CF 的证书（官方文档里面给的地址）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">wget https://developers.cloudflare.com/ssl/static/authenticated_origin_pull_ca.pem<br></code></pre></td></tr></table></figure><p>然后，把它重命名为 crt 后缀：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">mv</span> ./authenticated_origin_pull_ca.pem ./authenticated_origin_pull_ca.crt<br></code></pre></td></tr></table></figure><p>然后，配置你的 Nginx，根据你的证书保存位置加上两行（此处以放在全局 http 块中为例子。你也可以放在 server 块，为单个域名启用）：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><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 nginx"><span class="hljs-section">http</span> &#123;<br>    ...<br>    <span class="hljs-attribute">ssl_client_certificate</span> /etc/nginx/authenticated_origin_pull_ca.crt;<br>    <span class="hljs-attribute">ssl_verify_client</span> <span class="hljs-literal">on</span>;<br>    ...<br>    <span class="hljs-attribute">include</span> /etc/nginx/conf.d/<span class="hljs-regexp">*.conf</span>;<br><br>&#125;<br></code></pre></td></tr></table></figure><p>现在重载 Nginx：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> service nginx reload<br></code></pre></td></tr></table></figure><p>好了，访问你的域名，应该仍然可以正常访问。再试一次，用刚才的 curl 命令绕过 CF 直接访问，你就会很欣喜地发现一个 400 错误，提示没有提供客户端证书：</p><p><img src="https://img.samhou.top/1773151302799.webp" alt="错误"></p><p>现在你的源站防护等级升高了一个档次。</p><p>其实原理很简单：就是源站要求客户端提供证书，然后对证书进行了一次验证而已，和普通的 SSL 只是方向相反而已。</p><div class="note danger flat"><p><strong>Cloudflare 内部威胁</strong></p><p>由于使用了 Cloudflare 的证书，所以只能验证请求来自 CF 的边缘节点，但不能验证请求是你的账号所发出的。也就是说，如果别人通过某种方法让 CF 以你的 Host 回源你的服务器，就会绕过此限制。要解决这个问题，只能自定义上传证书，这不在本文的探讨范围内。</p></div>]]>
      </content:encoded>
    </item>
    <item>
      <title>水星冲浪日志 2 —— PT、换域名、约稿和建站机</title>
      <link>https://blog.samhou.moe/aqua-surf-2/</link>
      <description>《水星冲浪日志》的第二期，记录了博主初探 PT 下载站的经历，博客换域名的折腾，看板娘形象的确立和画师约稿的成果，以及新入手的高性能建站机器</description>
      <author>Sam Hou</author>
      <category domain="https://blog.samhou.moe/categories/talking/">杂谈</category>
      <category domain="https://blog.samhou.moe/tags/mjj/">mjj</category>
      <category domain="https://blog.samhou.moe/tags/pt/">pt</category>
      <category domain="https://blog.samhou.moe/tags/domain/">domain</category>
      <category domain="https://blog.samhou.moe/tags/art/">art</category>
      <category domain="https://blog.samhou.moe/tags/vps/">vps</category>
      <pubDate>Thu, 05 Mar 2026 11:03:02 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>距离上一篇水星冲浪日志已经过去一个月多了。因此，第二篇日志新鲜出炉~在这一个月多一点的时间里，网上冲浪和技术又发生了许多可以写的内容，甚至让我有些难以抉择了。</p><p>在开始之前，我们先来看看上一篇日志的评论区——收到了一条留言：</p><blockquote><p>关注一下你的装机，来自RSS订阅用户。</p></blockquote><p>嗯，这位读者可能要失望了，上一期组装好机器之后，作为做种机器了（见下方内容），<strong>目前暂时没有继续装机的打算</strong>。下一次装机，预计是等到家里云硬盘出现问题的时候了——不过估计不会太久，等到硬盘和内存价格降下来之后，我希望能<strong>自己组一台 NAS</strong>。毕竟，生命不息，折腾不止。</p><p>留言读完，事不宜迟，让我们开始吧！</p><h2 id="PT"><a href="#PT" class="headerlink" title="PT"></a>PT</h2><p>作为一个追求极致的看番观众，我经常会需要下载老番和 BD 版本（至少也要 1080p 高清）的动漫资源。</p><p>之前都是 bt 下载，然而发现大批吸血客户端吸血，此外还有一堆死种。然后在论坛里面知道了 PT 这个东西，正好有很多 vps 和两条联通宽带+大盘家里云，就开始玩 PT 了。</p><p>PT 对你的分享率是有要求的，保种更是可以获得魔力值换各种各样的东西。</p><p>于是趁着过年 PT 站开始注册，尝试入了几个 PT 站。</p><p>摸索一段时间后，我发现 PT 的正确打法：</p><ul><li>先看规则和常见问题</li><li>规则不允许盒子（vps 等国外大带宽公网设备）？那就用家里宽带下热门种子和免费种子</li><li>先刷上传。刷完上传再保种，下你自己想要的保，这样满足下载和魔力要求</li><li>可以认领，这样能获得很多魔力值</li></ul><p>不过，毕竟我也是个 P 龄才一个月的新人，PT 站也都不允许公开宣传它们，所以此处就略过不多说啦（当然有大佬看到此文想发药，也可以联系我）。</p><p>关于 PT 嘛，我个人是非常喜欢这种互利共赢的做法的——你把资源分发给别人，别人也给你，在 bt 的基础上，确保了所有人都在好好做贡献。显然，这是个比 peerbanhelper 反吸血更好的方法，毕竟所有数据都会被记录下来，你可以安心地做贡献，而不是害怕被吸血。</p><p>再转向下一个话题之前，先来说说——</p><h3 id="机器的归宿"><a href="#机器的归宿" class="headerlink" title="机器的归宿"></a>机器的归宿</h3><p>上一期，我提到过自己装了一台机器。作为一台备用服务器，我把它设置在了异地——另一条宽带所在的地区。虽然盘非常小，但是够用了，只是用来保点 PT 种子，很不错。一个 PT 站要活下去，确保种子活着很重要，因此保种+认领是可以获得魔力值的，非常划算。</p><h2 id="换域名"><a href="#换域名" class="headerlink" title="换域名"></a>换域名</h2><p>接下来就是博客的一件大事：</p><p>top 这个后缀总感觉不太舒服，而且听论坛大佬说对搜索引擎收录有影响，所以我决定——换成 moe 后缀！</p><p>moe 后缀其实早就注册了——小说站和一些项目用的就是 moe。现在只是需要把 top 迁移过来即可。</p><p>首先创建 301 跳转，指示已经永久移动，这个简单，vercel 甚至可以一键自动完成这个操作。至于带域名的链接的话，可以批量VSCode 替换，非常方便。</p><p>然后就是搜索引擎的迁移了。</p><p>Search Console 简单，直接申请换域名，自动检查通过即可。</p><p>Bing 没找到入口，于是给客服发了工单。客服说重定向了会自动建立索引，于是我就等了几天，还真是流量都回来了，看来这个过程确实很少需要人工介入。</p><p>最后也是最难的一步，通知友链更改。当然由于自动 301 重定向，不改也没事，一个个通知会浪费自己和对方的时间，所以就有点摆烂了，只通知了友链下方评论区，以及“开往”、博友圈、虚拟备案这种可以自主更改的朋友和实体。</p><p>如果有看到这篇文章的朋友，麻烦去更新一下友链信息哦，毕竟友链已经加了不少了。RSS 订阅也需要修改域名，只要把 top 改成 moe 即可。</p><p>更改过程非常丝滑，而且由于只是要改那些需要搜索引擎收录的站点，所以评论系统和 umami 数据分析不需要修改。大大减少了工作量，好评。</p><h2 id="约稿"><a href="#约稿" class="headerlink" title="约稿"></a>约稿</h2><p>接下来聊聊博客美术。</p><p>记得上一篇，我们提到了<a href="https://blog.samhou.moe/aqua-surf-1/#%E7%9C%8B%E6%9D%BF%E5%A8%98">看板娘的形象</a>。现在有点钱了，就找了一位老师约稿画了出来。作为一个理工男，想象力实在有限，我甚至害怕画师会因为我的设计太烂而拒绝。但是当我仔细构思过形象，然后把企划挂到平台上之后，很快就有老师来接稿了，于是就生成了这么个结果：</p><p><img src="https://img.samhou.top/seimouhornia.webp" alt="S 酱立绘（请尊重博主和画师的设计，勿复制&#x2F;转载&#x2F;喂给 AI）"></p><p>这个形象，以及修正后的设定，可在<a href="https://blog.samhou.moe/character/">设定集</a>查看。</p><p>看板娘的形象确定下来之后，接下来就有以下的设想：</p><ul><li>根据形象再约稿一个头像</li><li>把看板娘应用到某些博客文章中去</li><li>继续约稿完善博客的美术设计</li></ul><p>嗯，这是个耗钱的工作。<del>所以等赚到钱了再说，摆烂！</del></p><h2 id="建站机"><a href="#建站机" class="headerlink" title="建站机"></a>建站机</h2><p>最后，是上一篇 MJJ 的延续。</p><p>是的。MJJ 上瘾了。我决定抛弃之前那台香港的 2c4g 杂牌 VPS（天天断网谁受的住啊），然后买一台正经的稳定建站机。</p><p>于是找到了 netcup 这家便宜大碗的商家。经历了<a href="https://samhou.de/notes/aj8dlxjie5wy00u9" rel="external nofollow noreferrer">一点波折</a>后，论坛上收了一个机器，4c8g 512G 高性能德国机器。</p><p>然后，把服务迁移到了那里，现在整个博客的评论系统和 umami 数据分析就在这台机器上，大家可以体验一下套了 cf 之后的速度，应该还不错？</p><p>这台机器我会长期续费了，MJJ 建站毕业机器，除非炸鸡，否则真的不买了。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>这就是第二期日志的全部内容了，因为是公开的杂谈栏目所以话题受限，只有这么多可以写的东西，请期待下一期~</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>奶奶都能看懂的 C++ —— 函数指针、decltype、类型别名和尾置返回</title>
      <link>https://blog.samhou.moe/cpp-function-pointer-type/</link>
      <description>本文深入讲解了C++中的函数指针、decltype、类型别名和尾置返回类型。通过通俗易懂的实例，详细拆解了函数指针的声明、赋值、调用及其在参数传递、函数返回等场景的应用，并探讨了如何利用现代C++特性简化复杂类型声明，使代码更清晰灵活。</description>
      <author>Sam Hou</author>
      <category domain="https://blog.samhou.moe/categories/cpp-tutorial/">C++教程</category>
      <category domain="https://blog.samhou.moe/tags/cpp/">cpp</category>
      <category domain="https://blog.samhou.moe/tags/%E6%8C%87%E9%92%88/">指针</category>
      <category domain="https://blog.samhou.moe/tags/%E5%87%BD%E6%95%B0/">函数</category>
      <category domain="https://blog.samhou.moe/tags/decltype/">decltype</category>
      <category domain="https://blog.samhou.moe/tags/%E7%B1%BB%E5%9E%8B%E5%88%AB%E5%90%8D/">类型别名</category>
      <pubDate>Wed, 04 Mar 2026 06:03:01 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p><a href="https://blog.samhou.moe/cpp-overload-match/">上一节</a>我们讲了函数，这次来聊聊<em>函数指针</em>。</p><p>顾名思义，函数指针指的就是，<strong>指向函数的指针</strong>。也就是，指针解引用之后是一个可调用的函数。</p><p>先来看一段示例代码：</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">addInt</span><span class="hljs-params">(<span class="hljs-type">int</span> a, <span class="hljs-type">int</span> b)</span> </span>&#123;<br>    <span class="hljs-keyword">return</span> a + b;<br>&#125;<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>    <span class="hljs-built_in">int</span> (*pAddInt)(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>);<br>    pAddInt = addInt;<br>    cout &lt;&lt; <span class="hljs-built_in">pAddInt</span>(<span class="hljs-number">3</span>, <span class="hljs-number">4</span>) &lt;&lt; endl; <span class="hljs-comment">// 7</span><br>&#125;<br></code></pre></td></tr></table></figure><p>现在我们来仔细看看这段代码做了些什么。</p><h2 id="声明函数指针"><a href="#声明函数指针" class="headerlink" title="声明函数指针"></a>声明函数指针</h2><p>首先我们声明并定义了一个函数 <code>addInt</code>，它接受两个 <code>int</code> 类型的参数，然后返回它们的和。</p><p>然后我们来看今天的重点，main 函数。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-built_in">int</span> (*pAddInt)(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>);<br></code></pre></td></tr></table></figure><p>这就是我们所说的，<strong>函数指针的声明</strong>。</p><p>我们来看看为什么它是函数指针：</p><ul><li><code>pAddInt</code> 是这个变量的名称，我们从这里往外读</li><li>首先是括号，里面有一个 <code>*</code>，说明它是个指针</li><li>右侧的括号中有两个 <code>int</code> 类型，说明这是个参数列表</li><li>左侧的 int 表示一个返回值类型</li><li>因此，它是一个函数指针，可以指向一个函数，这个函数<strong>需要</strong>接受 2 个 int 类型的参数，并返回一个 int</li></ul><p>正如我们在介绍指针时所提到的，指针所指向的内容是有类型限定的，由指针的类型决定。函数指针也是如此，你看，上面的代码已经明确了允许指向的函数的要求了。</p><p>其实，函数指针就是函数加个星号而已，声明和正常的指针没什么差异：</p><figure class="highlight cpp"><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 cpp"><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">addInt</span><span class="hljs-params">(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>)</span></span>;<br><span class="hljs-built_in">int</span> (*pAddInt)(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>);<br></code></pre></td></tr></table></figure><p>再提醒你一下，在 C++ 中，括号的优先级是比 <code>*</code> 更高的，因此，声明函数指针的时候，必须给星号加上括号。比如，下面的声明是错误的，会声明一个<strong>返回值</strong>为<strong>指向 <code>int</code> 类型的指针</strong>的<strong>函数</strong>，而非<em>函数指针</em>：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">int</span> *<span class="hljs-title">pAddInt</span><span class="hljs-params">(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>)</span></span>;<br></code></pre></td></tr></table></figure><p>是不是看到了指向数组指针的影子？是的，由于括号问题，它们呈现出相似的视觉效果。</p><p>好了，现在我们声明了一个函数指针，但问题是，这个指针现在没有指向任何东西，行为是未定义的。</p><h2 id="赋值函数指针"><a href="#赋值函数指针" class="headerlink" title="赋值函数指针"></a>赋值函数指针</h2><p>我们来看下一行：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp">pAddInt = addInt;<br></code></pre></td></tr></table></figure><p>这一行代码将函数指针 <code>pAddInt</code> 指向了一个实际的函数 <code>addInt</code>。</p><p>但是你或许注意到了一个问题——指针指向的是一个地址，但是为什么这里没有取地址呢？</p><p>首先我们明确一点，函数确实有一个地址，指针也正是指向了这个内存地址。<strong>指针本身</strong>存储了地址，是一个对象，但是<strong>函数</strong>并不是对象（也不能赋值给变量）。</p><p>然后我们再来看为什么没有取地址符号 <code>&amp;</code> 这个问题。</p><p>其实你自己先试一下就会发现，加上这个符号也能通过编译，并正常运行：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp">pAddInt = &amp;addInt;<br></code></pre></td></tr></table></figure><p>这种灵活来自于 C++ 的<strong>退化</strong>性质，当一个表达式中存在函数名字的时候，这个函数会<strong>自动转换为函数指针</strong>。</p><p>是不是很耳熟？没错，这和<strong>数组</strong>是一个道理！还记得吗，我们在<a href="https://blog.samhou.moe/cpp-array-with-pointer/">数组与指针</a>这一节中，曾经提到过，数组在表达式中也会自动转换为指针，指向的是第一个元素。</p><p>函数也是类似，函数名字在表达式中自动转换为指向该函数的函数指针。因此，取地址符号是<strong>可以省略的</strong>。</p><h2 id="调用函数指针"><a href="#调用函数指针" class="headerlink" title="调用函数指针"></a>调用函数指针</h2><p>既然我们已经有了一个函数指针，是时候来看看怎么用了。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp">cout &lt;&lt; <span class="hljs-built_in">pAddInt</span>(<span class="hljs-number">3</span>, <span class="hljs-number">4</span>) &lt;&lt; endl;<br></code></pre></td></tr></table></figure><p>看到了吗？我们正在调用函数指针，正如调用普通的函数一样。</p><p>我知道你要说什么。你肯定想这么干：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp">cout &lt;&lt; (*pAddInt)(<span class="hljs-number">3</span>, <span class="hljs-number">4</span>) &lt;&lt; endl;<br></code></pre></td></tr></table></figure><p>你自己试试就会发现无论是否解引用，表现都是一致的。</p><p>这是因为，编译器为你承担起了这一切——和自动退化相<strong>对称</strong>，解引用也并不是必须的，我愿称之为一组<strong>对称法则</strong>（提示，这不是官方说法，是我个人的总结）。</p><p>实际上，函数指针<strong>可以作为返回值和参数</strong>，就如通常的指针一样。但是，在继续讨论函数指针前，我们先来讲解几个好用的东西，然后再继续深入。</p><h2 id="decltype"><a href="#decltype" class="headerlink" title="decltype"></a>decltype</h2><p>当我们涉及指针的时候，是不是发现声明语句越来越复杂了呢？一个变量的类型可能变得相当复杂：</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">int</span>* <span class="hljs-title">addInt</span><span class="hljs-params">(<span class="hljs-type">int</span> *a, <span class="hljs-type">int</span> *b)</span> </span>&#123;<br>    *a = *a + *b;<br>    <span class="hljs-keyword">return</span> a;<br>&#125;<br></code></pre></td></tr></table></figure><p>要创建指向上面函数的指针，我们必须使用这种类型声明语句：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-type">int</span>* (*pAddInt)(<span class="hljs-type">int</span>*, <span class="hljs-type">int</span>*);<br></code></pre></td></tr></table></figure><p>有没有什么简单的方法呢？当然，让我们隆重介绍 <code>decltype</code>！</p><p>看看这个：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">decltype</span>(addInt) *pAddInt;<br></code></pre></td></tr></table></figure><p><code>decltype</code> 可以把一个<strong>名字</strong>对应的类型，直接<strong>偷过来</strong>！我们这里偷来了 addInt 这个函数的类型（包括参数列表类型和返回值）。通常情况下，这样会声明一个新的函数，但由于我们加上了一个 <code>*</code>，所以我们现在声明的是函数指针。</p><p>一个非常重要的事情是，decltype <strong>不发生退化</strong>，所以记得带上 <code>*</code>。decltype 只是负责推算里面表达式的类型而已，不会帮你转换。</p><p>因此，使用 decltype 可以极大简化代码编写流程，让我们免去书写指针的类型的麻烦。</p><p>注意这个当然不局限于函数指针，而是对于任何指针都是有效的！</p><p>比如，指向数组的指针：</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-type">int</span> a[<span class="hljs-number">10</span>];<br><span class="hljs-keyword">decltype</span>(a) *p;<br><span class="hljs-built_in">int</span> (*p2)[<span class="hljs-number">10</span>];<br></code></pre></td></tr></table></figure><p>第二、三行声明的指针，类型都是一样的，都是指向含有 10 个 int 的数组的指针哦。</p><h2 id="类型别名"><a href="#类型别名" class="headerlink" title="类型别名"></a>类型别名</h2><p>明白了 decltype 能够偷来类型，我们再来讲一个能够把类型起个别名的东西，<em>类型别名</em>。</p><p><strong>类型别名</strong>，从名字上来看，就是用另外一个名字，替代原来的类型名字。</p><h3 id="typedef"><a href="#typedef" class="headerlink" title="typedef"></a>typedef</h3><p>传统的类型别名使用方法是 <code>typedef</code>，比如：</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">typedef</span> <span class="hljs-type">double</span> d;<br>d num = <span class="hljs-number">3.14</span>;<br>cout &lt;&lt; num &lt;&lt; endl;<br></code></pre></td></tr></table></figure><p>我们给 double 起了个别名叫做 d。</p><p>但这个太简单了，当类型变得非常复杂的时候，别名才会发挥出作用：</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">typedef</span> <span class="hljs-type">double</span>** d; <span class="hljs-comment">// 指向指针的指针类型</span><br><span class="hljs-keyword">typedef</span> <span class="hljs-type">int</span> arrType[<span class="hljs-number">10</span>]; <span class="hljs-comment">// 包含10个int的数组类型</span><br><span class="hljs-function"><span class="hljs-keyword">typedef</span> <span class="hljs-title">int</span> <span class="hljs-params">(*arrType)</span>[10]</span>; <span class="hljs-comment">// 一个指针类型，指向的是包含10个int的数组类型</span><br><span class="hljs-keyword">typedef</span> <span class="hljs-type">int</span> *arrType[<span class="hljs-number">10</span>]; <span class="hljs-comment">// 包含10个指向int的指针的数组类型</span><br></code></pre></td></tr></table></figure><p>你现在可能非常疑惑，为什么第一条和后面三条的语法<strong>看起来完全不一样</strong>（你看看，第一句的语法好像是 <code>typedef 类型 新的名字</code>，但后面的语法完全不是这样）？换句话说，typedef 到底是如何工作的？</p><p>别被迷惑了，让我们先把 typedef 本身移除掉——</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-type">double</span>** d; <span class="hljs-comment">// 指向指针的指针类型</span><br><span class="hljs-type">int</span> arrType[<span class="hljs-number">10</span>]; <span class="hljs-comment">// 包含10个int的数组类型</span><br><span class="hljs-built_in">int</span> (*arrType)[<span class="hljs-number">10</span>]; <span class="hljs-comment">// 一个指针类型，指向的是包含10个int的数组类型</span><br><span class="hljs-type">int</span> *arrType[<span class="hljs-number">10</span>]; <span class="hljs-comment">// 包含10个指向int的指针的数组类型</span><br></code></pre></td></tr></table></figure><p>现在你可以看出些端倪了：给类型取别名，和<strong>创建变量</strong>的本质没什么区别。</p><p>奶奶都知道可以这么创建变量：</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-type">double</span>** dp;<br><span class="hljs-type">int</span> arr[<span class="hljs-number">10</span>];<br><span class="hljs-built_in">int</span> (*arr)[<span class="hljs-number">10</span>];<br><span class="hljs-type">int</span> *arr[<span class="hljs-number">10</span>];<br></code></pre></td></tr></table></figure><p>嗯？是不是发现了什么呢？语法<strong>完全一致</strong>。</p><p>也就是说，typedef 和创建变量的区别只有一个，就是前者<strong>创建的名字是一个可以直接使用的类型</strong>。</p><p>比如：</p><figure class="highlight cpp"><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 cpp">d myd;<br>arrType myArr; <span class="hljs-comment">// 直接当作类型使用！</span><br></code></pre></td></tr></table></figure><p>好了，既然你已经知道如何创建类型别名，那么我们回到函数指针。同理，你可以这么创建函数指针类型：</p><figure class="highlight cpp"><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 cpp"><span class="hljs-function"><span class="hljs-keyword">typedef</span> <span class="hljs-title">int</span> <span class="hljs-params">(*funcPType)</span><span class="hljs-params">(<span class="hljs-type">int</span>*, <span class="hljs-type">int</span>*)</span></span>;<br>funcPType p1; <span class="hljs-comment">// 这是一个函数指针</span><br></code></pre></td></tr></table></figure><p>这样可以节省大量时间。</p><h3 id="using"><a href="#using" class="headerlink" title="using"></a>using</h3><p>在新版的 C++ 中，你可以用另一种方式创建类型别名：</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">using</span> d = <span class="hljs-type">double</span>;<br>d d1 = <span class="hljs-number">3.14</span>;<br>cout &lt;&lt; d1 &lt;&lt; endl; <span class="hljs-comment">// 3.14</span><br></code></pre></td></tr></table></figure><p>这个 using 会把等号后面的类型，起别名，别名名字为等号前的内容。我们可以把上面的 typedef 全部转换为 using：</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">using</span> d = <span class="hljs-type">double</span>** ; <span class="hljs-comment">// 指向指针的指针类型</span><br><span class="hljs-keyword">using</span> arrType = <span class="hljs-type">int</span>[<span class="hljs-number">10</span>]; <span class="hljs-comment">// 包含10个int的数组类型</span><br><span class="hljs-keyword">using</span> arrType = <span class="hljs-built_in">int</span> (*)[<span class="hljs-number">10</span>]; <span class="hljs-comment">// 一个指针类型，指向的是包含10个int的数组类型</span><br><span class="hljs-keyword">using</span> arrType = <span class="hljs-type">int</span>*[<span class="hljs-number">10</span>]; <span class="hljs-comment">// 包含10个指向int的指针的数组类型</span><br></code></pre></td></tr></table></figure><p>很简单吧？只是把名字前置了，相比 typedef，这种方式看起来更加方便。</p><p>当然，也别忘了我们的主角函数指针：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">using</span> funcPType = <span class="hljs-built_in">int</span> (*)(<span class="hljs-type">int</span>*, <span class="hljs-type">int</span>*);<br></code></pre></td></tr></table></figure><p>更强大的是，你可以把类型别名和 decltype 组合使用，避免写出复杂的类型：</p><figure class="highlight cpp"><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 cpp"><span class="hljs-keyword">using</span> funcType = <span class="hljs-keyword">decltype</span>(func); <span class="hljs-comment">// 现在，你给函数起了别名</span><br>funcType funcP = *funcType; <span class="hljs-comment">// 根据函数别名，创建对应的函数指针</span><br></code></pre></td></tr></table></figure><h2 id="传递函数指针"><a href="#传递函数指针" class="headerlink" title="传递函数指针"></a>传递函数指针</h2><p>既然函数指针本身，是个指针，那么它就变成了一个对象，自然可以传递，就如其它指针一样。</p><h3 id="作为参数传递"><a href="#作为参数传递" class="headerlink" title="作为参数传递"></a>作为参数传递</h3><p>那这有什么用呢？其实，这样可以让同一个函数根据传入的函数指针，执行不同的操作。</p><p>比如，下面的代码，将运算的函数指针传入，然后调用函数执行自定义操作：</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">sum</span><span class="hljs-params">(<span class="hljs-type">int</span> a, <span class="hljs-type">int</span> b)</span> </span>&#123;<br>    <span class="hljs-keyword">return</span> a + b;<br>&#125;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">diff</span><span class="hljs-params">(<span class="hljs-type">int</span> a, <span class="hljs-type">int</span> b)</span> </span>&#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-built_in">abs</span>(a - b);<br>&#125;<br><span class="hljs-keyword">using</span> funcPType = <span class="hljs-keyword">decltype</span>(sum)*;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">mathOperation</span><span class="hljs-params">(<span class="hljs-type">int</span> a, <span class="hljs-type">int</span> b, funcPType funcP)</span> </span>&#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-built_in">funcP</span>(a, b);<br>&#125;<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>    <span class="hljs-type">int</span> a = <span class="hljs-number">3</span>, b = <span class="hljs-number">4</span>;<br>    cout &lt;&lt; <span class="hljs-built_in">mathOperation</span>(a, b, sum) &lt;&lt; endl; <span class="hljs-comment">// 7</span><br>    cout &lt;&lt; <span class="hljs-built_in">mathOperation</span>(a, b, diff) &lt;&lt; endl; <span class="hljs-comment">// 1</span><br>&#125;<br></code></pre></td></tr></table></figure><p>花点时间好好理解一下上面的代码。</p><p>首先，写了两个执行算术操作的函数。</p><p>然后，我们创建了一个叫做 funcPType 的类型别名，它代表的类型是一个函数指针，指向接受 2 个 int 参数的返回 int 的函数。</p><p>之后，写了一个算术操作的函数，接受函数指针类型，在内部调用这个函数指针，执行相对应的操作。</p><p>最后写了 main 函数，两次传入不同的函数名字。函数名自动退化为函数指针，作为参数传入。你可以看到，我们的 a b 都没有变化，但是结果却不同。这正是因为传入了不同的函数指针导致的。</p><p>也就是说，<strong>函数指针可以把不同操作打包，交给其它部分操作</strong>。也就是，<strong>把行为作为数据传递</strong>。函数指针只记下了传入什么、返回什么，不关心实际执行了什么操作——也正因为如此，我们可以在需要函数指针的地方传入不同的函数，这极大地提升了我们程序的灵活性。</p><p>另外我要提醒你一点，<strong>不要给函数名字后面加上括号</strong>。这样会变成直接调用函数，传递的就是返回值了，会发生类型不匹配直接报错。我们要传递的，是会自动退化成函数指针的<strong>函数名字</strong>。</p><h3 id="作为返回值"><a href="#作为返回值" class="headerlink" title="作为返回值"></a>作为返回值</h3><p>不止参数。函数指针也可以作为<strong>返回值</strong>。</p><p>来看看下面的示例代码（略去和上述相同的 sum 和 diff 函数）：</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">using</span> funcPType = <span class="hljs-keyword">decltype</span>(sum)*;<br><span class="hljs-function">funcPType <span class="hljs-title">chooseOperation</span><span class="hljs-params">(<span class="hljs-type">char</span> c)</span> </span>&#123;<br>    <span class="hljs-keyword">if</span>(c == <span class="hljs-string">&#x27;+&#x27;</span>) <span class="hljs-keyword">return</span> sum;<br>    <span class="hljs-keyword">if</span>(c == <span class="hljs-string">&#x27;-&#x27;</span>) <span class="hljs-keyword">return</span> diff;<br>    <span class="hljs-keyword">return</span> <span class="hljs-literal">nullptr</span>;<br>&#125;<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>    <span class="hljs-type">char</span> c1 = <span class="hljs-string">&#x27;+&#x27;</span>;<br>    <span class="hljs-type">char</span> c2 = <span class="hljs-string">&#x27;-&#x27;</span>;<br>    funcPType choice = <span class="hljs-built_in">chooseOperation</span>(c1);<br>    cout &lt;&lt; <span class="hljs-built_in">choice</span>(<span class="hljs-number">1</span>, <span class="hljs-number">3</span>) &lt;&lt; endl; <span class="hljs-comment">// 4</span><br>    choice = <span class="hljs-built_in">chooseOperation</span>(c2);<br>    cout &lt;&lt; <span class="hljs-built_in">choice</span>(<span class="hljs-number">1</span>, <span class="hljs-number">3</span>) &lt;&lt; endl; <span class="hljs-comment">// 2</span><br>&#125;<br></code></pre></td></tr></table></figure><p>仔细看看，我们的函数根据传入的参数不同，返回了不同的函数指针。返回的指针可以作为函数调用——在两条输出语句那里，我们用的是同一个函数指针名称，但是它们指向的是<strong>不同的函数</strong>，也就导致了不同的执行结果。</p><p>这是一个非常现实的例子，同样也是把<strong>行为作为数据传递</strong>的体现。掌握了这种写法，你就可以大大提升你的程序的灵活度。</p><p>此外，我们也可以不用类型别名，但是这会出现一些非常复杂的东西，我们在下面的尾置返回类型中，进行进一步的探讨。</p><h2 id="尾置返回类型"><a href="#尾置返回类型" class="headerlink" title="尾置返回类型"></a>尾置返回类型</h2><p>关于函数指针我们说的够多了，现在我们再来补充一点 C++ 新版本的知识，<strong>尾置返回类型</strong>。</p><p>顾名思义，尾置返回类型允许我们把函数返回值的类型放在末尾。来看看这个：</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-keyword">auto</span> <span class="hljs-title">chooseOperation</span><span class="hljs-params">(<span class="hljs-type">char</span> c)</span> -&gt; funcPType </span>&#123;<br>    <span class="hljs-keyword">if</span>(c == <span class="hljs-string">&#x27;+&#x27;</span>) <span class="hljs-keyword">return</span> sum;<br>    <span class="hljs-keyword">if</span>(c == <span class="hljs-string">&#x27;-&#x27;</span>) <span class="hljs-keyword">return</span> diff;<br>    <span class="hljs-keyword">return</span> <span class="hljs-literal">nullptr</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>这是上面的代码的另一种写法。原本用于声明函数返回类型的首位被 auto 代替了，真正的返回类型写在最后。</p><p>你可能在想，这不是多此一举吗？其实，只有当你不想用类型别名，但是却想返回函数指针（或任何复杂的类型，比如指向数组的指针）的时候，你会发现这大大被简化了：</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-built_in">int</span> (*<span class="hljs-built_in">chooseOperation</span>(<span class="hljs-type">char</span> c))(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>) <span class="hljs-comment">// 一般情况</span><br><span class="hljs-function"><span class="hljs-keyword">auto</span> <span class="hljs-title">chooseOperation</span><span class="hljs-params">(<span class="hljs-type">char</span> c)</span> -&gt; <span class="hljs-title">int</span><span class="hljs-params">(*)</span><span class="hljs-params">(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>)</span> <span class="hljs-comment">// 尾置返回类型，这下看懂了</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">if</span>(c == <span class="hljs-string">&#x27;+&#x27;</span>) <span class="hljs-keyword">return</span> sum;<br>    <span class="hljs-keyword">if</span>(c == <span class="hljs-string">&#x27;-&#x27;</span>) <span class="hljs-keyword">return</span> diff;<br>    <span class="hljs-keyword">return</span> <span class="hljs-literal">nullptr</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>也补充一下，如果不用尾置返回类型，第一行</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-built_in">int</span> (*<span class="hljs-built_in">chooseOperation</span>(<span class="hljs-type">char</span> c))(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>)<br></code></pre></td></tr></table></figure><p>的含义是：</p><ul><li>从内向外阅读。首先 <code>chooseOperation(char c)</code> 说明此处是个函数。</li><li>其次 <code>(*...)</code> 表示它的返回值是个指针。</li><li>再次，<code>int(...)(int,int)</code> 表示这个指针指向的是个函数，函数接受 2 个 int，返回一个 int。</li></ul><p>如果你有些混乱，不妨翻到文章开头，看看函数指针是如何声明的，对比看看：</p><figure class="highlight cpp"><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 cpp"><span class="hljs-built_in">int</span> (*pAddInt)(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>);<br><span class="hljs-built_in">int</span> (*<span class="hljs-built_in">chooseOperation</span>(<span class="hljs-type">char</span> c))(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>)<br></code></pre></td></tr></table></figure><p>现在是不是发现了点规律？只是把变量名字换成函数名字+参数列表而已。</p><p>但是，我完全不建议你这么写。请一定要使用尾置返回类型、类型别名和 decltype，让你的代码更加可读。</p><p>关于函数指针和简化代码的方法，我们已经涉及了相当深入的内容了。下一节，我们将离开函数这一篇章，进入新的一部分——<strong>类</strong>。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>奶奶都能看懂的 C# —— LINQ、 Lambda 和 IEnumerable</title>
      <link>https://blog.samhou.moe/csharp-linq-lambda-enumerable/</link>
      <description>本文深入讲解 C# 中 LINQ 与 Lambda 表达式，介绍如何将声明式查询转换为方法链调用，详细拆解 Where、OrderBy、GroupBy 和 Join 等关键操作。最后通过实现 IEnumerable 接口与 yield return 机制，揭示 LINQ 查询的底层迭代原理。</description>
      <author>Sam Hou</author>
      <category domain="https://blog.samhou.moe/categories/csharp/">csharp</category>
      <category domain="https://blog.samhou.moe/tags/csharp/">csharp</category>
      <category domain="https://blog.samhou.moe/tags/linq/">linq</category>
      <category domain="https://blog.samhou.moe/tags/lambda/">lambda</category>
      <category domain="https://blog.samhou.moe/tags/ienumerable/">ienumerable</category>
      <pubDate>Wed, 04 Feb 2026 11:02:29 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p><a href="https://blog.samhou.moe/csharp-linq-guide/">上一篇</a>，我们讲了 LINQ 的入门知识，了解了方法和声明式查询这两种形式，但是对于将声明式查询的一部分，转换为方法，还没有深入了解。</p><p>这一篇，我们将从 Lambda 表达式开始，一步一步带你走进 LINQ 方法的世界，最终自己实现 IEnumerable！</p><p>让我们开始吧。</p><h2 id="Lambda-表达式"><a href="#Lambda-表达式" class="headerlink" title="Lambda 表达式"></a>Lambda 表达式</h2><p>在阅读 C# 代码的时候，你一定时常碰见这个 <code>=&gt;</code>。</p><p>比如：</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-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> <span class="hljs-built_in">string</span> <span class="hljs-title">ToString</span>()</span> =&gt; <span class="hljs-string">&quot;String&quot;</span>;<br></code></pre></td></tr></table></figure><p>上面的例子非常直观，你一定能猜到它是怎么工作的：让这个方法的返回值为 <code>&quot;String&quot;</code>。</p><p><code>=&gt;</code> 这个小符号，定义的就是一个 <em>Lambda 表达式</em>。它声明了一个<em>匿名函数</em>（没有名字的方法），我们可以把它的语法总结为：</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-number">1</span>, 参数类型 参数<span class="hljs-number">2</span> ...) =&gt; 表达式;<br></code></pre></td></tr></table></figure><p>括号内部的，是匿名函数的参数列表；表达式表示的值，就是这个匿名函数的<em>返回值</em>。</p><p>也就是说，我们可以把很多单个 return 的方法，借助 lambda 表达式，直接简化为一行！</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">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">AwesomeClass</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">string</span> <span class="hljs-title">Shout</span>(<span class="hljs-params"><span class="hljs-built_in">string</span> s</span>)</span> =&gt; <span class="hljs-string">&quot;OHHHHHHHHHHH THIS IS SIMPLE&quot;</span>;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">IsOverHundred</span>(<span class="hljs-params"><span class="hljs-built_in">int</span> num</span>)</span> =&gt; num &gt; <span class="hljs-number">100</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>第一个不再赘述，第二个的含义是，接受 num，然后计算 &#x3D;&gt; 后面的表达式，返回表达式的值（即，检测 num 是否大于100）</p><p><em>等等</em>。你一定在思考一个问题：刚才提到说，lambda 表达式创建的是<strong>匿名方法</strong>，但是上面的示例代码里面，不还是有名字吗？？这个<strong>匿名</strong>，到底是什么意思呢？</p><p>欸，实际上，lambda 表达式定义的是<strong>一段函数逻辑</strong>，本身确实是<strong>没有名字</strong>的。</p><p>但是，它可以用在<strong>任何需要一个函数体的地方</strong>，包括上面的 <code>ToString</code> 和 <code>Shout</code>。正是因为你把这个匿名方法用到了一个有名字的类成员身上，所以才<em>看起来</em>有名字。</p><p>也就是说，上面代码中，涉及 lambda 和匿名函数的，只有：</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">() =&gt; <span class="hljs-string">&quot;String&quot;</span>;<br>(<span class="hljs-built_in">string</span> s) =&gt; <span class="hljs-string">&quot;OHHHHHHHHHHH THIS IS SIMPLE&quot;</span>;<br><span class="hljs-comment">// 也就是……</span><br>(输入) =&gt; 输出;<br></code></pre></td></tr></table></figure><p>并不包含括号前面的那个名字。</p><p>那么问题来了，这和我们的 LINQ 又有什么关系呢？</p><h2 id="LINQ-中的-Lambda"><a href="#LINQ-中的-Lambda" class="headerlink" title="LINQ 中的 Lambda"></a>LINQ 中的 Lambda</h2><h3 id="入门：过滤-Where"><a href="#入门：过滤-Where" class="headerlink" title="入门：过滤 Where"></a>入门：过滤 Where</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></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">var</span> result = <span class="hljs-keyword">from</span> num <span class="hljs-keyword">in</span> numbers<br>             <span class="hljs-keyword">where</span> num &gt; <span class="hljs-number">500</span><br>             <span class="hljs-keyword">orderby</span> num<br>             <span class="hljs-keyword">select</span> num + <span class="hljs-number">1</span>;<br></code></pre></td></tr></table></figure><p>我们已经提到，声明式查询可以改写为方法串链。既然是直接在原来的集合上面调用，那么方法串链的形式不需要 from；那么，现在来试试输入 <code>Where()</code> 吧：</p><p><img src="https://img.samhou.top/1770188472527.webp" alt="智能提示"></p><p>来了点奇怪的东西！IDE 告诉我们，Where 这个方法，需要一个 <code>Func&lt;int,bool&gt;</code> 作为参数……？</p><p>点进去看一下（反编译）：</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-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">delegate</span> TResult <span class="hljs-title">Func</span>&lt;<span class="hljs-keyword">in</span> <span class="hljs-title">T</span>, <span class="hljs-keyword">out</span> <span class="hljs-title">TResult</span>&gt;(<span class="hljs-params">T arg</span>)</span>;<br></code></pre></td></tr></table></figure><blockquote><p><em>delegate</em>！这是一个<em>委托</em>。<br>关于委托，这并不在本文的讨论范围内。先用 LINQ 和 Lambda 的思维看看吧。</p></blockquote><p>你可以简单理解为，这里<strong>需要一个方法</strong>，以 T 作为参数，TResult 作为结果。就这么个简单的 LINQ 查询，你肯定不想独立写一个方法出来——因此，是时候让匿名函数出手了：</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">numbers.Where(num =&gt; num &gt; <span class="hljs-number">500</span>)<br></code></pre></td></tr></table></figure><p><code>Func&lt;int,bool&gt;</code> 表示参数类型 int，返回值类型 bool。上面的查询中需要<strong>一种逻辑</strong>：</p><p>“如果通过参数传入的 int <strong>满足某些条件</strong>，那么返回 true 保留；否则，返回 false 丢掉。”</p><p>因此我们写了下面这样的 lambda 表达式，让这个匿名函数对于参数大于 500 的情况返回 True，否则返回 False。</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">num =&gt; num &gt; <span class="hljs-number">500</span><br></code></pre></td></tr></table></figure><p>我知道你一定又有问题了。</p><p>明明我们的 lambda 表达式应该是这样的：</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">(参数类型 参数) =&gt; 表达式;<br></code></pre></td></tr></table></figure><p>为什么括号没了，甚至参数类型也没了！？？</p><p>这是因为——如果这个 lambda 作为一个参数 (<code>Func&lt;T,T&gt;</code>) 传入另一个方法，那么这个匿名函数的<em>参数</em>和<em>返回值</em>的<strong>类型</strong>，是<strong>确定已知</strong>的，因此，<strong>没有必要</strong>去写参数的类型。</p><p>而又由于只有一个参数名称，所以括号是不必要的，可以删除，最终得到这样的简化式子：</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">参数 =&gt; 表达式<br></code></pre></td></tr></table></figure><p>要提醒的是，千万别忘了，lambda 的参数和方法一样，是要<strong>自己命名的</strong>，这里省略的是参数类型，不是参数名字本身！</p><p>因此，<strong>当你看到 <code>Func&lt;T,T&gt;</code> 类型的参数的时候，请明白，这里需要一个处理逻辑，也就是一个 lambda 表达式</strong>。</p><h3 id="排序-Orderby"><a href="#排序-Orderby" class="headerlink" title="排序 Orderby"></a>排序 Orderby</h3><p>声明式查询的第二个子句，是 orderby。</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">orderby</span> num<br></code></pre></td></tr></table></figure><p>现在我们在刚才的 Where 后面，串接上 orderby：</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">numbers.Where((<span class="hljs-built_in">int</span> num) =&gt; num &gt; <span class="hljs-number">500</span>)<br>    .OrderBy(num=&gt;num);<br></code></pre></td></tr></table></figure><p>来自己试试吧！把鼠标悬浮在 orderby 上面，你会发现它需要的是 <code>Func&lt;int,TKey&gt;</code> 类型的一个参数。</p><p>TKey 是啥？看看注释：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">... in ascending order according to a key.<br></code></pre></td></tr></table></figure><p>哦！我们知道了，我们需要一种逻辑，传入一个列表元素，返回一个可以排序的东西（叫做 key）。然后 orderby 会根据这个 key，排序原始传入的列表元素。</p><p>在我们的情况下，由于数据源是 int 类型列表，所以传入的对象是 int。</p><p>仔细想想之后，会发现——我们<strong>就是要根据这个 int 类型的列表元素本身</strong>，来进行排序！</p><p>所以根本就不需要进行任何处理，直接返回 num 本身不就好了？</p><p>所以……</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">num =&gt; num<br></code></pre></td></tr></table></figure><p>我知道这个看起来多此一举，但是总不能什么都不填……这个 lambda 的含义是，接收到参数命名为 num，按照原样返回 num！</p><p>实际开发中肯定不是排序数字列表这么简单，再给你举一个不那么“多此一举”的例子（继续拿上一篇中的 User 类）：</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-built_in">enum</span> Status<br>&#123;<br>    Offline,<br>    Online<br>&#125;<br><span class="hljs-keyword">class</span> <span class="hljs-title">User</span><br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> Id &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>    <span class="hljs-keyword">public</span> Status Status &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>如果有个 User 列表，就能这么写：</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">users.OrderBy(user=&gt;user.Id)<br></code></pre></td></tr></table></figure><p>这里的含义是，把 users 列表，按照其每个 User 类实例（命名为 user）的 Id 排序。</p><p>我们把其等价声明式查询拿出来——</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">from</span> user <span class="hljs-keyword">in</span> users <span class="hljs-keyword">orderby</span> user.Id <span class="hljs-keyword">select</span> user<br></code></pre></td></tr></table></figure><p>比对一下吧！把每个部分这么一对应，是不是声明式查询和方法查询都变得十分清晰了呢？</p><h3 id="分组-Group"><a href="#分组-Group" class="headerlink" title="分组 Group"></a>分组 Group</h3><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><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 csharp">User[] users = <span class="hljs-keyword">new</span> User[]<br>&#123;<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">1</span>, Status = Status.Online &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">2</span>, Status = Status.Online &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">3</span>, Status = Status.Offline &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">4</span>, Status = Status.Online &#125;,<br>&#125;;<br><span class="hljs-keyword">var</span> result=<span class="hljs-keyword">from</span> user <span class="hljs-keyword">in</span> users <span class="hljs-keyword">orderby</span> user.Id<br>           <span class="hljs-keyword">group</span> user <span class="hljs-keyword">by</span> user.Status <span class="hljs-keyword">into</span> userGroup<br>           <span class="hljs-keyword">select</span> userGroup;<br><span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> <span class="hljs-keyword">group</span> <span class="hljs-keyword">in</span> result)<br>&#123;<br>    Console.WriteLine(<span class="hljs-string">&quot;Group &quot;</span> + <span class="hljs-keyword">group</span>.Key);<br>    <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> <span class="hljs-keyword">group</span>)<br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;User ID #&quot;</span> + item.Id<br>            + <span class="hljs-string">&quot; Status: &quot;</span> + item.Status);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>先自己尝试一下吧！先按照上面的讲解写出 OrderBy()，然后输入 GroupBy()，把鼠标放上去查看它需要接受一个怎样的参数。</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></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">var</span> result = users.OrderBy(user =&gt; user.Id)<br>    .GroupBy(user =&gt; user.Status);<br></code></pre></td></tr></table></figure><p>写出来了吗？</p><p>现在来拆解一下：</p><p>GroupBy 接受一个 <code>Func&lt;User,TKey&gt;</code> 参数，含义是输入一个 User，根据 lambda 中匿名函数返回的 TKey 类型的数据来分组。相同的 TKey 类型分到一组。因此，我们这里是根据 user.Status 进行分组，TKey 这个泛型就变成了 Status enum。</p><p>好了，不啰嗦了。Group 和 Orderby 实在非常相似，自己对照一下，相信你一定可以明白。</p><h3 id="合并-Join"><a href="#合并-Join" class="headerlink" title="合并 Join"></a>合并 Join</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></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-comment">// 这个类没有改</span><br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> Id &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>    <span class="hljs-keyword">public</span> Status Status &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>&#125;<br><span class="hljs-keyword">class</span> <span class="hljs-title">Message</span> <span class="hljs-comment">// 表示用户发送的一条信息</span><br>&#123;<br>    <span class="hljs-comment">// 发送者 ID</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> SenderId &#123;  <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>    <span class="hljs-comment">// 发送内容</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> Text &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125; = <span class="hljs-string">&quot;&quot;</span>;<br>&#125;<br></code></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><code class="hljs csharp">User[] users = <span class="hljs-keyword">new</span> User[] <span class="hljs-comment">// 不变</span><br>&#123;<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">1</span>, Status = Status.Online &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">2</span>, Status = Status.Online &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">3</span>, Status = Status.Offline &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">4</span>, Status = Status.Online &#125;,<br>&#125;;<br>Message[] messages = <span class="hljs-keyword">new</span> Message[] <span class="hljs-comment">// 来构造一个示例消息列表</span><br>&#123;<br>    <span class="hljs-keyword">new</span> Message &#123;SenderId= <span class="hljs-number">1</span>,Text=<span class="hljs-string">&quot;I love this.&quot;</span>&#125;,<br>    <span class="hljs-keyword">new</span> Message &#123;SenderId= <span class="hljs-number">2</span>,Text=<span class="hljs-string">&quot;No wayyyyy we can leave message&quot;</span> &#125;,<br>    <span class="hljs-keyword">new</span> Message&#123;SenderId=<span class="hljs-number">3</span>,Text=<span class="hljs-string">&quot;OMG this is crazy&quot;</span>&#125;,<br>    <span class="hljs-keyword">new</span> Message&#123;SenderId=<span class="hljs-number">3</span>,Text=<span class="hljs-string">&quot;Great work!&quot;</span>&#125;,<br>    <span class="hljs-keyword">new</span> Message&#123;SenderId=<span class="hljs-number">4</span>,Text=<span class="hljs-string">&quot;Can I delete my message???&quot;</span>&#125;<br>&#125;;<br><span class="hljs-keyword">var</span> result = <span class="hljs-keyword">from</span> message <span class="hljs-keyword">in</span> messages<br>             <span class="hljs-keyword">join</span> user <span class="hljs-keyword">in</span> users<br>             <span class="hljs-keyword">on</span> message.SenderId <span class="hljs-keyword">equals</span> user.Id<br>             <span class="hljs-keyword">select</span> <span class="hljs-keyword">new</span><br>             &#123;<br>                 SenderId = message.SenderId,<br>                 Text = message.Text,<br>                 UserStatus = user.Status,<br>             &#125;;<br><span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> result)<br>&#123;<br>    Console.WriteLine(<span class="hljs-string">&quot;Message [&quot;</span> + item.Text +<br>        <span class="hljs-string">&quot;] from user #&quot;</span> + item.SenderId +<br>        <span class="hljs-string">&quot; whose status is &quot;</span> + item.UserStatus);<br>&#125;<br></code></pre></td></tr></table></figure><p>现在我们来看这个 Join——来试试输入吧！</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">messages.Join( <span class="hljs-comment">// 试着打这些</span><br></code></pre></td></tr></table></figure><p>IDE 里面智能提示太长了放不下！我们去 <a href="https://learn.microsoft.com/zh-cn/dotnet/api/system.linq.enumerable.join" rel="external nofollow noreferrer">Microsoft Learn</a> 查一下这个方法（阅读文档是一种非常好的学习方式）。</p><blockquote><p>Correlates the elements of two sequences based on matching keys.</p></blockquote><p>由于这是一个<em>扩展方法</em>（此处不展开），所以带着 this 的参数直接忽略，它表示 .Join 前面的那个对象。</p><p>还剩下 4 个参数：</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">System.Collections.Generic.IEnumerable&lt;TInner&gt; inner,<br>Func&lt;TOuter,TKey&gt; outerKeySelector, <br>Func&lt;TInner,TKey&gt; innerKeySelector, <br>Func&lt;TOuter,TInner,TResult&gt; resultSelector<br></code></pre></td></tr></table></figure><blockquote><p>Inner: The sequence to join to the first sequence.</p></blockquote><p>也就是说，第一个参数是需要拼接到目标对象的序列。我们的情境下，是 <code>users</code> 序列。</p><p>下面的两个参数是：</p><blockquote><p>A function to extract the join key from each element of the first sequence.<br>A function to extract the join key from each element of the second sequence.</p></blockquote><p>哦~理解了，就是给两个序列，分别写两个 lambda 表达式，返回两个属性，会判断它们是否匹配！也就是：</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">on</span> message.SenderId <span class="hljs-keyword">equals</span> user.Id<br></code></pre></td></tr></table></figure><p>那么，我们写出这样的 lambda 表达式就行了：</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">message=&gt;message.SenderId<br>user=&gt;user.Id<br></code></pre></td></tr></table></figure><p>最后当然就是 select 啦，我们就在这里创建新的匿名类型：</p><blockquote><p>A function to create a result element from two matching elements.</p></blockquote><p><strong>注意了！由于我们有两个序列，所以需要 2 个参数的匿名方法。两个参数就不可以省略括号了。</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">(message, user) =&gt; <span class="hljs-keyword">new</span><br>    &#123;<br>        SenderId = user.Id,<br>        Text = message.Text,<br>        UserStatus = user.Status,<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></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">var</span> result = messages.Join(users, message =&gt; message.SenderId,<br>    user =&gt; user.Id, (message, user) =&gt; <span class="hljs-keyword">new</span><br>    &#123;<br>        SenderId = user.Id,<br>        Text = message.Text,<br>        UserStatus = user.Status,<br>    &#125;);<br></code></pre></td></tr></table></figure><p>你觉得哪种，声明式，还是直接写方法比较舒服呢？其实这取决于你自己——写出来的代码只要清晰易懂即可。</p><h2 id="实现-IEnumerable"><a href="#实现-IEnumerable" class="headerlink" title="实现 IEnumerable"></a>实现 IEnumerable</h2><h3 id="手写实现接口"><a href="#手写实现接口" class="headerlink" title="手写实现接口"></a>手写实现接口</h3><p>对于 LINQ，现在你已经有相当深入的了解了。但是，你还记得我在上一篇开头的地方，给你留的小剧透吗？</p><blockquote><p>在本文的后半，会介绍怎么自己实现这个接口，不过现在我们先记住这个前提，然后来用一下 LINQ 感受一下吧！</p></blockquote><p>对于数据查询，你已经几乎离不开 LINQ 了不是吗？</p><p>因此，你当然希望你的数据——我是指，你自己创建的类，也应该能够实现 LINQ 查询对吧。</p><p>现在让我们来探索这个接口。</p><p>比如，我们来创建一个自定义的用户列表 UserList 类型！</p><p>键入下面的 class 语句：</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">class</span> <span class="hljs-title">UserList</span> : <span class="hljs-title">IEnumerable</span>&lt;<span class="hljs-title">User</span>&gt;<br></code></pre></td></tr></table></figure><p>现在在波浪线的地方按下 alt + enter，选中“实现接口”。</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">class</span> <span class="hljs-title">UserList</span> : <span class="hljs-title">IEnumerable</span>&lt;<span class="hljs-title">User</span>&gt;<br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> IEnumerator&lt;User&gt; <span class="hljs-title">GetEnumerator</span>()</span><br>    &#123;<br>        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotImplementedException();<br>    &#125;<br>    IEnumerator IEnumerable.GetEnumerator()<br>    &#123;<br>        <span class="hljs-keyword">return</span> GetEnumerator();<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>现在你知道这个接口必须实现这两个方法。那么问题来了，IDE 生成的这个 <code>IEnumerator&lt;User&gt;</code> 接口（更通用地，是 <code>IEnumerator&lt;T&gt;</code>），又是什么？？？</p><p>从名字上理解，这个叫做<strong>枚举器</strong>，也就是把一个集合里面的东西<strong>一个个枚举出来</strong>的方法。</p><blockquote><p>如果你看过《<a href="https://blog.samhou.moe/cpp-vector-iterator-guide/">奶奶都能看懂的 C++ —— vector 与迭代器</a>》这篇文章或者你是 C++ 高手，一个很好的方法就是，把枚举器看成 C++ 迭代器，但是不需要解引用。（没看过也没关系！下面会从零开始详细解释枚举器）</p></blockquote><p>我们不妨来创建一个类，来让 IDE 实现这个接口：</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">class</span> <span class="hljs-title">UserListEnumerator</span> : <span class="hljs-title">IEnumerator</span>&lt;<span class="hljs-title">User</span>&gt;<br>&#123;<br>    <span class="hljs-keyword">public</span> User Current =&gt; <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotImplementedException();<br>    <span class="hljs-built_in">object</span> IEnumerator.Current =&gt; Current;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Dispose</span>()</span><br>    &#123;<br>        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotImplementedException();<br>    &#125;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">MoveNext</span>()</span><br>    &#123;<br>        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotImplementedException();<br>    &#125;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Reset</span>()</span><br>    &#123;<br>        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotImplementedException();<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>WOW，出来了一堆方法！</p><p>我们来结合含义讲解一下。</p><ul><li>Current、Move、Reset，看得出来，好像有什么在移动</li><li>没错，这个接口表示的就是一个<strong>枚举器</strong>，会从序列开头移动到末尾</li><li>Current 返回枚举器当前指向的对象</li><li>MoveNext 把枚举器移到下一位，如果移位后 Current 指向的对象有效，那么返回 ture；如果已经越过列表末尾了，那么返回 false</li><li>Reset 重置枚举器</li><li>Dispose 会释放枚举器使用的资源（此处略去，不在讨论范围内）</li></ul><p>现在让我们来实现！</p><p>但是，我们的 UserList 明明自称是一个可枚举的列表，里面却没有数据。由于这个程序只是一个演示用途，因此，我们通过构造函数传入，来初始化这个列表：</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">class</span> <span class="hljs-title">UserList</span> : <span class="hljs-title">IEnumerable</span>&lt;<span class="hljs-title">User</span>&gt;<br>&#123;<br>    User[] _users;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">UserList</span>(<span class="hljs-params">User[] users</span>)</span><br>    &#123;<br>        <span class="hljs-comment">// 传入！</span><br>        _users = users;<br>    &#125;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> IEnumerator&lt;User&gt; <span class="hljs-title">GetEnumerator</span>()</span><br>    &#123;<br>        <span class="hljs-comment">// 根据现有数据生成枚举器！</span><br>        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> UserListEnumerator(_users);<br>    &#125;<br>    IEnumerator IEnumerable.GetEnumerator()<br>    &#123;<br>        <span class="hljs-keyword">return</span> GetEnumerator();<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>再来手动实现枚举器的逻辑——注意，<strong>第一次迭代</strong>的时候就会调用 MoveNext，因此下标从 -1 开始：</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">class</span> <span class="hljs-title">UserListEnumerator</span> : <span class="hljs-title">IEnumerator</span>&lt;<span class="hljs-title">User</span>&gt;<br>&#123;<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> User[] _users;<br>    <span class="hljs-built_in">int</span> _index = <span class="hljs-number">-1</span>; <span class="hljs-comment">// 从 -1 开始！</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">UserListEnumerator</span>(<span class="hljs-params">User[] users</span>)</span><br>    &#123;<br>        _users = users;<br>    &#125;<br>    <span class="hljs-keyword">public</span> User Current =&gt; _users[_index]; <span class="hljs-comment">// 当前的元素</span><br>    <span class="hljs-built_in">object</span> IEnumerator.Current =&gt; Current;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Dispose</span>()</span><br>    &#123;<br>    <span class="hljs-comment">// 略去。</span><br>    &#125;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">MoveNext</span>()</span><br>    &#123;<br>        <span class="hljs-comment">// 移到下一位</span><br>        _index++;<br>        <span class="hljs-comment">// 检查移位之后，当前的 Current 是否有效</span><br>        <span class="hljs-keyword">return</span> _index &lt; _users.Length;<br>    &#125;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Reset</span>()</span><br>    &#123;<br>        _index = <span class="hljs-number">-1</span>;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>确实，手动写这么一大堆东西，头都大了，有什么更加简单方便的方法来实现枚举器吗？</p><h3 id="yield-return"><a href="#yield-return" class="headerlink" title="yield return"></a>yield return</h3><p>当然！隆重介绍 <code>yield return</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></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">UserList</span> : <span class="hljs-title">IEnumerable</span>&lt;<span class="hljs-title">User</span>&gt;<br>&#123;<br>    User[] _users;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">UserList</span>(<span class="hljs-params">User[] users</span>)</span><br>    &#123;<br>        _users = users;<br>    &#125;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> IEnumerator&lt;User&gt; <span class="hljs-title">GetEnumerator</span>()</span><br>    &#123;<br>        <span class="hljs-keyword">for</span>(<span class="hljs-built_in">int</span> i = <span class="hljs-number">0</span>; i &lt; _users.Length; i++)<br>        &#123;<br>            <span class="hljs-keyword">yield</span> <span class="hljs-keyword">return</span> _users[i];<br>        &#125;<br>    &#125;<br>    IEnumerator IEnumerable.GetEnumerator()<br>    &#123;<br>        <span class="hljs-keyword">return</span> GetEnumerator();<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>你可能已经发现，有一个类全部消失了！这就是 yield return，它可以全自动生成一个 <code>IEnumerator&lt;User&gt;</code>。</p><p>但是<em>等等</em>。</p><blockquote><p>这到底是什么原理？我枚举这个列表的时候，代码究竟在怎么执行？为什么 yield return 可以返回一个枚举器？</p></blockquote><p>其实，yield return 就是两句话：</p><ul><li>枚举时，yield return 返回当前枚举的元素，然后保存这个执行状态</li><li>到下一次迭代时，从上一次 yield return 处，继续执行。</li></ul><p>不妨来做个实验吧！我们把 yield return 上下改成这样：</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">for</span>(<span class="hljs-built_in">int</span> i = <span class="hljs-number">0</span>; i &lt; _users.Length; i++)<br>&#123;<br>    Console.WriteLine(<span class="hljs-string">&quot;Start Yield return #&quot;</span>+(i+<span class="hljs-number">1</span>));<br>    <span class="hljs-keyword">yield</span> <span class="hljs-keyword">return</span> _users[i];<br>    Console.WriteLine(<span class="hljs-string">&quot;End Yield return #&quot;</span>+(i+<span class="hljs-number">1</span>));<br>&#125;<br></code></pre></td></tr></table></figure><p>然后写一个 foreach 方法（没错，实现了 IEnumerable 就能用 foreach 了）</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">UserList userList=<span class="hljs-keyword">new</span>(users);<br><span class="hljs-keyword">foreach</span> (User user <span class="hljs-keyword">in</span> userList)<br>&#123;<br>    Console.WriteLine(<span class="hljs-string">&quot;Getting user #&quot;</span> + user.Id);<br>&#125;<br></code></pre></td></tr></table></figure><p>试试看吧！</p><figure class="highlight text"><table><tr><td class="gutter"><pre><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 text">Start Yield return #1<br>Getting user #1<br>End Yield return #1<br>Start Yield return #2<br>Getting user #2<br>End Yield return #2<br>Start Yield return #3<br>Getting user #3<br>End Yield return #3<br>Start Yield return #4<br>Getting user #4<br>End Yield return #4<br></code></pre></td></tr></table></figure><p>看到了吗？枚举的时候发生了这样的事情：</p><ul><li>获取第一个元素，执行到第一次 yield return，中断</li><li>获取第二个元素，从上次中断处继续执行，直到遇到下一次 yield return</li><li>……</li></ul><p>因此，返回的不是一个东西，而是一组：yield return 允许你创建一个<strong>全自动管理的</strong>枚举器，不需要手写，会根据需要执行——每一次 yield return，都会吐出一个元素，都是一次<strong>中断与恢复</strong>的过程。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>对于 LINQ，我们已经进行了非常深入的讲解，还涉及到了 lambda 表达式，以及 yield return 的核心原理。希望你有所收获！</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>奶奶都能看懂的 C# —— 手把手 LINQ</title>
      <link>https://blog.samhou.moe/csharp-linq-guide/</link>
      <description>本文以易懂方式介绍C# LINQ，涵盖查询前提IEnumerable、基础查询、声明式语法、分组与合并操作，并说明延迟执行机制。</description>
      <author>Sam Hou</author>
      <category domain="https://blog.samhou.moe/categories/csharp/">csharp</category>
      <category domain="https://blog.samhou.moe/tags/csharp/">csharp</category>
      <category domain="https://blog.samhou.moe/tags/linq/">linq</category>
      <pubDate>Sun, 01 Feb 2026 09:03:07 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>LINQ，学了会，会了忘，忘了学，学了又会，会了又忘……</p><p>受不了了！今天这篇文章就来讲解一下 C# 中的 LINQ，先入门，然后带你从<strong>代码内涵</strong>上，帮助记忆 LINQ 语法。</p><h2 id="标准-LINQ-的使用前提"><a href="#标准-LINQ-的使用前提" class="headerlink" title="标准 LINQ 的使用前提"></a>标准 LINQ 的使用前提</h2><p>在 C# 的世界里，有许许多多各式各样的对象，也有这些对象的集合，比如我们熟悉的 <code>List&lt;T&gt;</code>。</p><p>LINQ 就是为了对这些集合类型的数据进行查询和处理而生的。它的全名叫做 <code>Language-Integrated Query</code>，语言中集成的查询，很好的名字不是吗？</p><p>在正式开始查询之前，我们先来说说什么情况下能用。</p><p>动动手，打开 VS 创建一个 <code>List</code> 实例，把光标放在这个类型上面，然后按下 F12 导航到定义（反编译源码）。仔细观察这种“集合”类型的对象实现了哪些接口：</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">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">List</span>&lt;<span class="hljs-title">T</span>&gt; : <span class="hljs-title">ICollection</span>&lt;<span class="hljs-title">T</span>&gt;, <span class="hljs-title">IEnumerable</span>&lt;<span class="hljs-title">T</span>&gt;, <span class="hljs-title">IEnumerable</span>, <span class="hljs-title">IList</span>&lt;<span class="hljs-title">T</span>&gt;, <span class="hljs-title">IReadOnlyCollection</span>&lt;<span class="hljs-title">T</span>&gt;, <span class="hljs-title">IReadOnlyList</span>&lt;<span class="hljs-title">T</span>&gt;, <span class="hljs-title">ICollection</span>, <span class="hljs-title">IList</span><br></code></pre></td></tr></table></figure><p>嗯，有个非常有趣的东西 <code>IEnumerable&lt;T&gt;</code>。</p><p>你一定知道 I 表示它是个接口，后面尖括号里面的 T 表示泛型，现在我们来看看它的命名：</p><p><code>Enumerable</code>，可枚举的。字面意思理解，就是这个类里面有一堆东西，你可以一个一个遍历枚举出来（记得 foreach 循环吗？我们经常使用这种枚举来执行 List 操作）。</p><p>而标准 LINQ 的使用前提就是类实现了该接口（或能够隐式转换），很符合直觉，如果你这个类都无法枚举，还怎么查询数据呢？</p><p>在本文的后半，会介绍怎么自己实现这个接口，不过现在我们先记住这个前提，然后来用一下 LINQ 感受一下吧！</p><h2 id="快速开始查询数据"><a href="#快速开始查询数据" class="headerlink" title="快速开始查询数据"></a>快速开始查询数据</h2><p>要开始使用 LINQ，只需一行 using 即可，此时你会发现，对于实现了<code>IEnumerable</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">using</span> System.Linq;<br>...<br>List&lt;<span class="hljs-built_in">int</span>&gt; numbers = <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">114</span>, <span class="hljs-number">514</span>, <span class="hljs-number">233</span>, <span class="hljs-number">322</span>, <span class="hljs-number">44432</span>, <span class="hljs-number">23232</span> &#125;;<br><span class="hljs-keyword">var</span> result = numbers.Take(<span class="hljs-number">2</span>);<br><span class="hljs-comment">// 1</span><br><span class="hljs-comment">// 114</span><br><span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> number <span class="hljs-keyword">in</span> result)<br>&#123;<br>    Console.WriteLine(number); <br>&#125;<br><span class="hljs-comment">// 23232</span><br>Console.WriteLine(numbers.Last());<br></code></pre></td></tr></table></figure><p>上面的代码，用了 <code>Take</code> <code>Last</code> 这两个 LINQ 方法，顾名思义，take 取出头部的几个元素，last 取得最后单个元素。</p><p>当然 LINQ 的强大之处是使用<strong>方法串链</strong>，一次完成 2 步查询：</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> result = numbers.Take(<span class="hljs-number">2</span>).Last();<br>Console.WriteLine(result); <span class="hljs-comment">// 114</span><br></code></pre></td></tr></table></figure><p>先取出前两个，再取出结果中最后一个！</p><p>怎么样，是不是已经有点<strong>查询</strong>的样子了？你可以自己去看一下 IDE 的智能提示，里面还有许多查询方法可用。</p><h2 id="声明式查询"><a href="#声明式查询" class="headerlink" title="声明式查询"></a>声明式查询</h2><h3 id="基本声明式查询"><a href="#基本声明式查询" class="headerlink" title="基本声明式查询"></a>基本声明式查询</h3><p>但是等等！LINQ 不仅支持通过方法使用（其实这叫做<em>扩展方法</em>，但并不在这篇文章的讨论范围内），还支持<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></pre></td><td class="code"><pre><code class="hljs csharp">List&lt;<span class="hljs-built_in">int</span>&gt; numbers = <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">114</span>, <span class="hljs-number">514</span>, <span class="hljs-number">233</span>, <span class="hljs-number">322</span>, <span class="hljs-number">44432</span>, <span class="hljs-number">23232</span> &#125;;<br><span class="hljs-keyword">var</span> result = <span class="hljs-keyword">from</span> num <span class="hljs-keyword">in</span> numbers<br>             <span class="hljs-keyword">where</span> num &gt; <span class="hljs-number">500</span><br>             <span class="hljs-keyword">orderby</span> num<br>             <span class="hljs-keyword">select</span> num + <span class="hljs-number">1</span>;<br><span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> result)<br>&#123;<br>    Console.WriteLine(item);<br>&#125;<br></code></pre></td></tr></table></figure><p>把上面的代码键入到 Main 方法尝试一下吧！先根据含义猜测一下会输出什么。</p><figure class="highlight text"><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 text">515<br>23233<br>44433<br></code></pre></td></tr></table></figure><p>猜对了吗？这就是一个非常简单的 <strong>LINQ 声明式查询</strong>示例，这里的查询没有一点<strong>调用方法</strong>的样子！</p><p>现在我们来仔细拆解一下这到底是怎么工作的。我们可以把整个查询拆开成几部分（称为<em>子句</em>）：</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">from</span> num <span class="hljs-keyword">in</span> numbers<br></code></pre></td></tr></table></figure><p>先看这一部分。from 子句（数据从哪里来？）首先从要查询的对象里面“取”出数据，存入变量。in （从什么里面取？）指定查询的对象，然后存入 from 后面的变量。这个 num 是一个<strong>范围变量</strong>，有一个范围，里面是多个 int，迭代时会<strong>处理每一个变量</strong>。</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">where</span> num &gt; <span class="hljs-number">500</span><br></code></pre></td></tr></table></figure><p>再看这个 where 子句。它的含义就非常清晰好记：过滤出大于 500 的数据。</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">orderby</span> num<br></code></pre></td></tr></table></figure><p>再看 orderby 子句。这个子句会对列表内的数据从低到高排序。因为 num 是一个 int 类型，所以可以直接排序。</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">select</span> num + <span class="hljs-number">1</span>;<br></code></pre></td></tr></table></figure><p>最后是 select。select 会生成最后<strong>添加到</strong>列表的数据。这里的含义是，把经过前面子句处理的列表中，每个数据加上 1。</p><p>虽然整个查询看起来你都知道在说什么，但是我们还得深入挖一挖，这样你才能写自己的 LINQ 声明式查询。</p><p>首先要明确一点，声明式查询语句，是在声明一种<strong>对应关系</strong>。当查询执行的时候，C# 会<strong>遍历</strong> from 子句中 in 后面的列表，然后对每一个元素执行你的查询。</p><p>我们可以这么理解查询的过程（这只是帮助你理解，不代表 C# 真正的运行逻辑）：</p><ul><li>首先，<code>from num in numbers</code> 遍历取出 numbers 的每一个元素生成临时列表。然后，声明一下，以后每次遍历都创建叫做 num 的变量（也就是令下面的语句，都以 num 来自 numbers 作为前提）</li><li>然后，<code>where num &gt; 500</code> 过滤出大于 500 的数据。刚才提到遍历的变量叫做 num，因此类似 foreach 的循环遍历检查刚才的列表，过滤出结果。</li><li>过滤后的元素列表来到 <code>orderby num</code>。由于是 int 类型，默认数字从低到高排序。</li><li>然后，<code>select num + 1;</code>，遍历刚才排序后的列表中的每一个元素，全部 +1。</li></ul><blockquote><p>where orderby select 都很清晰啊，就是这个 from 也太难记忆了吧！怎么办！？</p></blockquote><p>其实，我们得理解它语义上的含义，这样才好记。</p><p>from，从……来。in，在……里面。from 表示的就是一种从哪里来，到哪里去的<strong>映射关系</strong>。也就是说，告诉 LINQ：这个查询使用 numbers 集合作为数据来源，遍历时各个成员都要用 num 表示。</p><h3 id="分组查询-group"><a href="#分组查询-group" class="headerlink" title="分组查询 group"></a>分组查询 group</h3><p>既然我们已经知道了查询的基础方法，现在可以来点更加深入的东西——比如，LINQ 对查询的结果可以进行分组。先来看看下面的一个 enum 和一个 User 类。</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-built_in">enum</span> Status<br>&#123;<br>    Offline,<br>    Online<br>&#125;<br><span class="hljs-keyword">class</span> <span class="hljs-title">User</span><br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> Id &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>    <span class="hljs-keyword">public</span> Status Status &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#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></pre></td><td class="code"><pre><code class="hljs csharp">User[] users = <span class="hljs-keyword">new</span> User[]<br>&#123;<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">1</span>, Status = Status.Online &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">2</span>, Status = Status.Online &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">3</span>, Status = Status.Offline &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">4</span>, Status = Status.Online &#125;,<br>&#125;;<br><span class="hljs-keyword">var</span> result=<span class="hljs-keyword">from</span> user <span class="hljs-keyword">in</span> users <span class="hljs-keyword">orderby</span> user.Id<br>           <span class="hljs-keyword">group</span> user <span class="hljs-keyword">by</span> user.Status <span class="hljs-keyword">into</span> userGroup<br>           <span class="hljs-keyword">select</span> userGroup;<br></code></pre></td></tr></table></figure><p>查询的第一行你肯定已经明白了，就是按照 ID 从小到大排序。</p><p>现在我们来拆解这个分组。首先，我们必须明白：这个 var，到底是什么类型呢？</p><p>把鼠标悬浮在上面，IDE 已经给出了答案：</p><figure class="highlight plaintext"><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 plaintext">...IEnumerable&lt;out T&gt;<br>T 是 IGrouping&lt;Status, User&gt;<br></code></pre></td></tr></table></figure><p>也就是，分组结果是个 IEnumerable，一个内含多个 IGrouping 的可迭代列表。那么问题来了，里面的元素，IGrouping 又是什么？</p><p>按住 Ctrl 点一下，转到定义：</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-comment">// Represents a collection of objects that have a common key.</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IGrouping</span>&lt;<span class="hljs-keyword">out</span> <span class="hljs-title">TKey</span>, <span class="hljs-keyword">out</span> <span class="hljs-title">TElement</span>&gt; : <span class="hljs-title">IEnumerable</span>&lt;<span class="hljs-title">TElement</span>&gt;, <span class="hljs-title">IEnumerable</span><br>&#123;<br>    TKey Key &#123; <span class="hljs-keyword">get</span>; &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>它又是实现了 IEnumerable 的一组数据，里面有一个 Key。</p><p>也就是说，在我们的情境下，IGrouping 是一个<strong>分组</strong>，里面包含<strong>多个 User 元素</strong>，这些 User 元素有<strong>相同的 Key</strong>（也就是相同的 Status，是分组的依据）。明白了这一点，下面的查询就不难理解了：</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">group</span> user <span class="hljs-keyword">by</span> user.Status <span class="hljs-keyword">into</span> userGroup<br></code></pre></td></tr></table></figure><p>这个子句的含义是：</p><ul><li>把 user 对应的列表根据 Status 分组，相同 Status 的 User 分到一组</li><li>每个 IGrouping 表示一个分组，包含多个 User 元素</li><li>Status 作为每个 IGrouping 分组的 Key（by 后面的就是 key）</li><li>最后把这些 IGrouping 分组塞到一个新的集合中，叫做 userGroup。</li></ul><p>看看下面的<strong>图片</strong>吧，瞬间秒懂。</p><p><img src="https://img.samhou.top/1769930042879.webp" alt="示意图"></p><p>这下就简单了！我们可以用两层 foreach 循环，来验证一下：</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">foreach</span> (<span class="hljs-keyword">var</span> <span class="hljs-keyword">group</span> <span class="hljs-keyword">in</span> result)<br>&#123;<br>    Console.WriteLine(<span class="hljs-string">&quot;Group &quot;</span> + <span class="hljs-keyword">group</span>.Key);<br>    <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> <span class="hljs-keyword">group</span>)<br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;User ID #&quot;</span> + item.Id<br>            + <span class="hljs-string">&quot; Status: &quot;</span> + item.Status);<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></pre></td><td class="code"><pre><code class="hljs csharp">Group Online<br>User ID <span class="hljs-meta">#1 Status: Online</span><br>User ID <span class="hljs-meta">#2 Status: Online</span><br>User ID <span class="hljs-meta">#4 Status: Online</span><br>Group Offline<br>User ID <span class="hljs-meta">#3 Status: Offline</span><br></code></pre></td></tr></table></figure><p>是不是和我们的预想完全一样呢？当然，我们这里是拿 Enum 作为 key，这只是一个比较符合现实、又合理的例子。</p><p>LINQ 会把 <strong>Key 完全相同</strong>的元素分到一组中。所以，你当然可以用其它类型——比如相同的 int 类型，把相同年龄的 User 分到一组（<del>除非有非常好的理由，真的有人会这么干吗？</del>）。在实际编写中，明智地选择 Key 是得到清晰的分组的必要条件。</p><h3 id="合并查询-join"><a href="#合并查询-join" class="headerlink" title="合并查询 join"></a>合并查询 join</h3><p>使用 LINQ 的时候，我们不禁在思考：可不可以让数据来自<strong>多个数据源</strong>呢？这些数据的<strong>某一个属性的值完全一样</strong>，难道就不能把它们合并到一起吗？</p><p>当然是可以的。LINQ 有一个 join 子句，能够帮你达成合并的任务。首先，我们要举两个示例模型。仔细看，确保你理解这两个类的结构和它们的现实含义：</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">class</span> <span class="hljs-title">User</span> <span class="hljs-comment">// 这个类没有改</span><br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> Id &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>    <span class="hljs-keyword">public</span> Status Status &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>&#125;<br><span class="hljs-keyword">class</span> <span class="hljs-title">Message</span> <span class="hljs-comment">// 表示用户发送的一条信息</span><br>&#123;<br>    <span class="hljs-comment">// 发送者 ID</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> SenderId &#123;  <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>    <span class="hljs-comment">// 发送内容</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> Text &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125; = <span class="hljs-string">&quot;&quot;</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>我们假设这是个用户留言板系统，所有的数据都是合法的，那么，一定会有以下的结论：</p><ul><li>系统中有一个<strong>用户列表</strong>，表示所有注册用户</li><li>还有一个<strong>消息列表</strong>，表示留言板上面的所有消息</li><li>每条消息都<strong>对应</strong>一个用户（一个用户可能发送多条消息）</li></ul><p>那么，我们可以把 User 列表，<strong>合并</strong>到 Message 里面！</p><p>我们先来构造点示例数据，然后展示 join 语句的用法：</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">User[] users = <span class="hljs-keyword">new</span> User[] <span class="hljs-comment">// 不变</span><br>&#123;<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">1</span>, Status = Status.Online &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">2</span>, Status = Status.Online &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">3</span>, Status = Status.Offline &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">4</span>, Status = Status.Online &#125;,<br>&#125;;<br>Message[] messages = <span class="hljs-keyword">new</span> Message[] <span class="hljs-comment">// 来构造一个消息列表</span><br>&#123;<br>    <span class="hljs-keyword">new</span> Message &#123;SenderId= <span class="hljs-number">1</span>,Text=<span class="hljs-string">&quot;I love this.&quot;</span>&#125;,<br>    <span class="hljs-keyword">new</span> Message &#123;SenderId= <span class="hljs-number">2</span>,Text=<span class="hljs-string">&quot;No wayyyyy we can leave message&quot;</span> &#125;,<br>    <span class="hljs-keyword">new</span> Message&#123;SenderId=<span class="hljs-number">3</span>,Text=<span class="hljs-string">&quot;OMG this is crazy&quot;</span>&#125;,<br>    <span class="hljs-keyword">new</span> Message&#123;SenderId=<span class="hljs-number">3</span>,Text=<span class="hljs-string">&quot;Great work!&quot;</span>&#125;,<br>    <span class="hljs-keyword">new</span> Message&#123;SenderId=<span class="hljs-number">4</span>,Text=<span class="hljs-string">&quot;Can I delete my message???&quot;</span>&#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></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">var</span> result = <span class="hljs-keyword">from</span> message <span class="hljs-keyword">in</span> messages<br>             <span class="hljs-keyword">join</span> user <span class="hljs-keyword">in</span> users<br>             <span class="hljs-keyword">on</span> message.SenderId <span class="hljs-keyword">equals</span> user.Id<br>             <span class="hljs-keyword">select</span> <span class="hljs-keyword">new</span><br>             &#123;<br>                 SenderId = message.SenderId,<br>                 Text = message.Text,<br>                 UserStatus = user.Status,<br>             &#125;;<br><span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> result)<br>&#123;<br>    Console.WriteLine(<span class="hljs-string">&quot;Message [&quot;</span> + item.Text +<br>        <span class="hljs-string">&quot;] from user #&quot;</span> + item.SenderId +<br>        <span class="hljs-string">&quot; whose status is &quot;</span> + item.UserStatus);<br>&#125;<br></code></pre></td></tr></table></figure><p>嗯，来仔细瞧一瞧，先画个图感受过程：</p><p><img src="https://img.samhou.top/1769935128889.webp" alt="合并示意图"></p><ul><li>from 子句从 messages 里面取出所有消息</li><li>join 子句从 users 列表中取出 user</li><li>如何合并？on … equals … 添加了限制条件。遍历 from 子句生成的临时 message 列表时，会根据条件进行比较。这里的条件是，on 和 equals 后面的内容完全一致</li><li>根据条件，当 user 的 id 和 message 中的 SenderId 相同时，user 和对应的 message 匹配成功。</li><li>合并之后，现在一个元素里面既有 message，又有 user。</li><li>select 子句创建的新的类型，根据之前已经匹配成功得到的临时列表，从每个合并后的元素中遍历取出 message.SenderId message.Text user.Status，然后创建新的对象</li></ul><p>好了，问题来了：我们 select 里面创建的到底是个啥？这个类型我们从来没见过啊？</p><p>没错，我们就是创建的新的类型，但是这个类型根本就没有名字，所有的属性都是只读的，这称作<em>匿名类型</em>。我们一直用 new 来创建新的类型，通常 new 后面需要一个类型名称。当我们省略这个类型名称的时候，我们就创建了一个<strong>匿名类型</strong>。</p><p>你已经知道，我们用 var 来让 C# 自己决定类型，省心省力。实际上，匿名类型也是 var 的重要用法！这是因为，匿名类型<strong>必须使用 var 关键字</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></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">var</span> testObj = <span class="hljs-keyword">new</span> &#123;<br>    Name = <span class="hljs-string">&quot;Sam&quot;</span>,<br>    Id = <span class="hljs-number">3</span><br>&#125;<br></code></pre></td></tr></table></figure><p>回到 LINQ。来看 select：</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">select</span> <span class="hljs-keyword">new</span><br>&#123;<br>    SenderId = message.SenderId,<br>    Text = message.Text,<br>    UserStatus = user.Status<br>&#125;<br></code></pre></td></tr></table></figure><p>这就是一个匿名类型！用 foreach 遍历的时候，我们只能用 var：</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">foreach</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> result)<br>&#123;<br>    Console.WriteLine(<span class="hljs-string">&quot;Message [&quot;</span> + item.Text +<br>        <span class="hljs-string">&quot;] from user #&quot;</span> + item.SenderId +<br>        <span class="hljs-string">&quot; whose status is &quot;</span> + item.UserStatus);<br>&#125;<br></code></pre></td></tr></table></figure><p>现在我们来运行一下刚才的整个程序：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><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 text">Message [I love this.] from user #1 whose status is Online<br>Message [No wayyyyy we can leave message] from user #2 whose status is Online<br>Message [OMG this is crazy] from user #3 whose status is Offline<br>Message [Great work!] from user #3 whose status is Offline<br>Message [Can I delete my message???] from user #4 whose status is Online<br></code></pre></td></tr></table></figure><p>完美！这下彻底把两组数据合并了。</p><p>需要特别注意的一点是，join 后面加进来的列表中的数据是会<strong>匹配</strong>的。因此，join 列表中的元素<strong>可能被复制</strong>（一个 user 合并到多条 message），比如上面的 user#3，两条 message 中都有同样的 id 和 status。</p><p>此外，多次使用 join 也是可以的！可以把多个列表合并到一起，此处不再赘述。</p><h2 id="懒计算"><a href="#懒计算" class="headerlink" title="懒计算"></a>懒计算</h2><p>我们刚才一直提到“声明”，其实这和 LINQ 的行为也是一致的。在你<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></pre></td><td class="code"><pre><code class="hljs csharp">List&lt;<span class="hljs-built_in">int</span>&gt; numbers = <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">114</span>, <span class="hljs-number">514</span>, <span class="hljs-number">233</span>, <span class="hljs-number">322</span>, <span class="hljs-number">44432</span>, <span class="hljs-number">23232</span> &#125;;<br><span class="hljs-keyword">var</span> result= <span class="hljs-keyword">from</span> num <span class="hljs-keyword">in</span> numbers<br>            <span class="hljs-keyword">where</span> num &gt;=<span class="hljs-number">115</span><br>            <span class="hljs-keyword">orderby</span> num<br>            <span class="hljs-keyword">select</span> num;<br>numbers[<span class="hljs-number">1</span>] = <span class="hljs-number">115</span>;<br><span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> result)<br>&#123;<br>    Console.WriteLine(item); <span class="hljs-comment">// 115 233 322 514 23232 44432</span><br>&#125;<br></code></pre></td></tr></table></figure><p>执行一下，你会发现结果中有一个 115！这明明就是在查询后面才添加到原始的 numbers 列表中才对……为什么会这样呢？</p><p>这是因为，使用 LINQ 的时候，把它赋值给一个变量并不会触发查询，直到赋值的这个变量被用到的时候，才会真正发生查询。这被称为<strong>懒计算</strong>。你在写 LINQ 只是声明了一种查询的<strong>方法</strong>，并非<em>触发了查询</em>。</p><h2 id="预告：Lambda-表达式"><a href="#预告：Lambda-表达式" class="headerlink" title="预告：Lambda 表达式"></a>预告：Lambda 表达式</h2><p>我们之前已经提到，方法和声明式查询<strong>都是标准 LINQ 的一种</strong>。实际上，它们就是<strong>完全一致、可以替换</strong>的关系！</p><p>如果刚才的声明式查让你感到有些疑惑，来换种角度看看LINQ吧。不过，在此之前，我们必须了解一个东西：Lambda 表达式，这样才能写出那些方法。</p><p>但是，这篇文章已经足够长了，因此，我们下次再讨论 lambda 以及 linq，敬请期待！</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>水星冲浪日志 1 —— 杂谈、MJJ、看板娘和装机</title>
      <link>https://blog.samhou.moe/aqua-surf-1/</link>
      <description>《水星冲浪日志》的第一期，记录了这个杂谈栏目的起源，博主初探 mjj 的过程，看板娘的前世今生，以及学习装机的全过程。</description>
      <author>Sam Hou</author>
      <category domain="https://blog.samhou.moe/categories/talking/">杂谈</category>
      <category domain="https://blog.samhou.moe/tags/mjj/">mjj</category>
      <category domain="https://blog.samhou.moe/tags/%E8%A3%85%E6%9C%BA/">装机</category>
      <category domain="https://blog.samhou.moe/tags/%E7%9C%8B%E6%9D%BF%E5%A8%98/">看板娘</category>
      <category domain="https://blog.samhou.moe/tags/oc/">oc</category>
      <category domain="https://blog.samhou.moe/tags/%E6%9D%82%E8%B0%88/">杂谈</category>
      <pubDate>Tue, 20 Jan 2026 04:01:30 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>经常来这个网站拜访的老朋友（<del>真的有吗</del>）都知道，这个博客都是注重深入讲解，聚焦一个主题进行实践的。</p><p>但是！既然是博客，总要有点博客的样子。</p><p>于是，这个杂谈栏目诞生了——《水星冲浪日志》。在这个分类中，我不会涉及很多专业技术知识，尽量让所有人都可以无门槛的看懂（<del>但有些梗和整活可能不行</del>），因此可以说是一个比较轻松的栏目，可以当成不定期更新的报纸一类的东西来看？总之不会深入任何一个话题，放松就好啦，这个栏目和整个网站的其它部分有很大的区别。</p><p>（可以用左边的 RSS 来订阅，也可以扫二维码公众号订阅）</p><p>在开始第一期之前，我打算先来谈谈这个杂谈栏目的名字，<em>水星冲浪日志</em>，到底是什么含义？</p><h2 id="杂谈栏目《水星冲浪日志》"><a href="#杂谈栏目《水星冲浪日志》" class="headerlink" title="杂谈栏目《水星冲浪日志》"></a>杂谈栏目《水星冲浪日志》</h2><p>《水星冲浪日志》这个名字起源于脑海里的一个想法。</p><p>把上网冲浪时候看到有意思的东西记录下来，分享出去——上网冲浪日记。</p><p>但这样也太直白了，必须取一个更加特别，更加好记、有特色的名字。就在这时，阅番目录里面的 <em>ARIA</em>（《水星领航员》）引起了我的注意。</p><p>水星？众所周知，大部分网络论坛都是灌水的好去处，网友们都喜欢灌水……那么，上网冲浪，不就是在水星冲浪了么？</p><p>既然这个栏目也大部分情况下涉及的会是和技术有关的东西，所以我们把日记这个词，再改成更符合技术视角的<strong>日志</strong>——</p><p>完美的命名诞生了！《水星冲浪日志》。</p><p>在这里，希望能留下点什么，记录整个学习、生活的有趣瞬间。这些文字不好分类，因此被归类到日志中……</p><p>关于名字已经说的够多了，让我们开始吧！</p><h2 id="MJJ（买鸡鸡）"><a href="#MJJ（买鸡鸡）" class="headerlink" title="MJJ（买鸡鸡）"></a>MJJ（买鸡鸡）</h2><p><del>沉迷 MJJ 无法自拔。</del></p><p>事情的起因是我觉得博客的评论区太慢了（经常断连），然后希望能够扔掉那台自称三网优化的 2c4g 年抛优惠虚拟主机，扔掉 Azure 和 Digital Ocean 的年抛学生机，找一个至少能稳定活下去的机器，于是注册了<del>万恶之源</del> NodeSeek 账号。</p><p>我的一个朋友给我推荐了 legendvps，但是到手之后发现这家伙线路真的不太行，只能当落地用（现在准备放生了）。</p><p>在论坛混了几天之后，突然像中邪了一样想搞一台加速网站访问的线路机器，要求我的联通宽带能够快速连接。</p><p>然后发了个帖子询问，最终买了台 JP 的的 Softbank 机器。<del>结果第二天就炸鸡了</del>。速度还不错，用起来还是比较爽的。</p><p>于是，MJJ 之旅开始了，现在看到什么机器都想买，即使用不到也要买<del>钱包要不保了</del>。还在家里的老笔记本搭的家里云上部署了一个 N8N 工作流，24 小时 AI 监控 NS 论坛的优惠和感兴趣的帖子。</p><p>感觉完全忘记了初衷……博客的评论区至今还是没有迁移，打算等年抛机快过期的时候，再买个传家宝高性能建站机套上 Edgeone CDN，迁移到那台主机上面去。</p><div class="note info flat"><p><strong>VPS 机器的分类</strong></p><p>按照连接链路，机器分为线路机（访问速度快，有国内优化）、落地机（IP 质量好）；按照网络，有一类特殊的机器叫 NAT 机（多个机器共用 IP，端口需要转发）；按照硬盘大小，大硬盘的叫做大盘鸡；按照用途，用于建站的，稳定、性能强的机器叫做建站机。由于这是个公开的博客，考虑到一些神秘的原因，所以有些细节在文本中无法涉及……总之，这一部分请脑补！</p></div><h2 id="看板娘"><a href="#看板娘" class="headerlink" title="看板娘"></a>看板娘</h2><p>在这篇日志的第二部分，我想来谈谈博客看板娘的角色设定。</p><p>由于设定集是和这篇文章同时上传的，所以在写本文时，这篇设定除了我和一个朋友外没有人看过。虽然我不是文科生，想象力也不太够，但是还是先放在这里：<a href="https://blog.samhou.moe/character/">设定集</a>。你或许也注意到了，现在网站右上角的导航栏关于界面变成了下拉菜单，里面增加了设定集的链接。</p><p>创造这个角色的初衷，是想要拥有自己在网络上的一种虚拟的数字身份（<del>类似 Vtuber</del>），让这个博客在各位客人的脑海里留下更加深刻的印象，同时在博客文章里面用起来增强趣味性。</p><p>于是，这个和我的网名完全一致的 OC（Original Character，原创角色）诞生了，目前作为博客的看板娘存在于设定集中。</p><p>感觉网站头图一直用其它画师的无授权作品实在不太好，虽然她们都比较大方没有找上门，但是我的良心受到了谴责。用 AI 又味道不对，或许最终还得是自己约稿才行吧。</p><p><del>V 点钱给我找画师约稿把看板娘画出来</del></p><h2 id="装机"><a href="#装机" class="headerlink" title="装机"></a>装机</h2><p>在这篇杂谈的最后，来说说一点最近的新突破吧！</p><p>起因是家里有一台超级古董的惠普一体机，淘汰下来吃灰好几年了。</p><p>于是，作为计科的学生，我决定不止停留在软件开发上，还要去学习硬件知识。然后就把这台一体机拆成碎片了……</p><p>电动手枪钻真的是世界上最伟大的发明！我这种<del>肌无力的</del>人都可以轻松拧开任何螺丝。</p><p><img src="https://img.samhou.top/1768881226731.webp" alt="碎片"></p><p>从中回收了 CPU，是 i3-4170t。</p><p>拆开了研究清楚了，但是接下来的任务就是动手 DIY 一台新的机器。现在硬件（内存、硬盘）价格都飙到天上去了，况且我几年前的高性能品牌 PC 还没有出现任何问题，所以新买一整套完全没有必要且不现实。因此，这次装机单纯是学习用途，能点亮能用即为成功，关键是为以后的 DIY 积累经验。</p><p>在一个老友的指导下，创建了以下装机单：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><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 text">cpu i3 4170t 0r 旧电脑拆机<br>主板华硕h81m-k 二手65r<br>内存金士顿3代4g*2 二手共40r<br>散热讯钛 17.2r<br>电源长城200w 二手23r<br>硬盘500g辣鸡机械 0r 旧电脑拆机<br>工业风格机箱 21.9r<br>硅脂 5.16r vga线 4.9r 电源线 8.53r sata线 1r<br>显示器辣鸡老显示器 键盘古董米物 鼠标雷柏太大了手控制不了闲置鼠标 全部0r<br></code></pre></td></tr></table></figure><p>然后就是静静地等待到货了，这台练手机器甚至只需要 200 块。</p><p>装机过程还是比较顺利的，看了硬件茶谈的 3D 演示视频，没有出什么岔子。然后就是激动人心的亮机时刻——</p><p><img src="https://img.samhou.top/1768882687547.webp" alt="放一会局域网 Jellyfin 试试看"></p><p>这价位，当个影音娱乐小主机还不错，打游戏还是别想了……</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>总之，这就是一个轻松愉悦的杂谈栏目，什么都会写，我也不太会局限于技术，会天马行空一点。那么，下期《水星冲浪日志》再会！</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>博客被 DDoS —— 复盘、分析和应对</title>
      <link>https://blog.samhou.moe/ddos-blog-improvement/</link>
      <description>你没听错。确实是我这个小破站被打了。网站火了（物理），被 DDoS 攻击，本文回顾了一系列事件，以及应对措施</description>
      <author>Sam Hou</author>
      <category domain="https://blog.samhou.moe/categories/web-development/">web开发</category>
      <category domain="https://blog.samhou.moe/tags/ddos/">ddos</category>
      <category domain="https://blog.samhou.moe/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/">网络安全</category>
      <pubDate>Mon, 05 Jan 2026 05:02:05 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>你没听错。确实是我这个小破站被打了。</p><p>众所周知，我的博客是个部署在 Vercel 上面的静态网站，只有评论区和 DL 是动态的。</p><p><del>网站火了</del></p><p>故事要从一月三号开始（<del>刚刚新年没几天就给我整了波大的</del>）</p><h2 id="两眼一抹黑"><a href="#两眼一抹黑" class="headerlink" title="两眼一抹黑"></a>两眼一抹黑</h2><p>第一波攻击发生的时间是 1&#x2F;3 下午 14 点左右。请求数量达到了惊人的 3M 个。</p><p>其实我一开始并不知道这件事，直到下午 5 点的时候瞄了眼 Vercel 面板才发现不对劲。怎么 100GB 流量用了 70G，1M 请求烧了 80%？？？</p><p>然后我就打开防火墙看看到底发生了什么——</p><p><img src="https://img.samhou.top/1767589396713.png" alt="被打了"></p><p>吓得我一激灵，期末周也不管了<del>把高数书扔了</del>，直接开始分析到底发生了什么。</p><p>面板后台就不放了，毕竟有些东西比较敏感（<del>其实是没给 Vercel 打钱，没保存历史记录</del>）。我一顿分析，发现攻击者的路径是我的头像：</p><p><code>/img/SamHou.png</code></p><p>好好好，刷流量来了，找了个比较大的文件。</p><h2 id="初步应对"><a href="#初步应对" class="headerlink" title="初步应对"></a>初步应对</h2><p>我承认这点是我的失策——图片本来应该放在对象存储（图床），然后套一层 CF &#x2F; Edgeone &#x2F; ESA 来防护的，我却为了速度和省事直接放在了 Vercel 项目里面，和网站 html 文本放在了一起，这无疑提高了单次攻击消耗的资源价值。</p><p>所以还能咋办，删了，扔到对象存储里面。</p><p><img src="https://img.samhou.top/1767589791224.png" alt="当时的提交"></p><p>还有一点，我之前对爬虫都是很宽容的，Bot 全部选择 Log 而不是 JS 质询或拦截。结果导致上面有 400K 次请求，刷流量的机器人没被拦截，烧掉了宝贵的流量。现在直接切换成拦截</p><h2 id="一波未平，一波又起"><a href="#一波未平，一波又起" class="headerlink" title="一波未平，一波又起"></a>一波未平，一波又起</h2><p>就在我洗洗睡了之后，攻击又开始了。</p><p>晚上 11 点左右，就在我躺在床上<del>安详地</del>刷手机的时候，攻击又开始了。</p><p><img src="https://img.samhou.top/1767590054930.png" alt="第二波 DDoS"></p><p>这次几乎全部请求被拦截或质询，攻击者攻击的内容仍然是那个图片文件，但是我删了，所以请求状态码都是 4XX，看来我的防护措施是起了一点效果的。</p><p>但是很不幸的是，没有被成功识别为 DDoS 的，但<strong>成功拦截的 Bot</strong> 在 Vercel 里面也是计费的，因此，Vercel 免费额度彻底用完了：</p><p><img src="https://img.samhou.top/1767590202740.png" alt="（悲）"></p><p>咱也不知道 Vercel 什么时候会停机……</p><h2 id="迁移"><a href="#迁移" class="headerlink" title="迁移"></a>迁移</h2><p>既然我们都受到这么大量的攻击了，那么 Vercel 已经不再适合作为博客托管平台了。CF 在国内访问速度太慢，排除。因此，我只能给迁移到 Edgeone Pages。虽然速度稍微慢一点，但至少访问在合理范围内不限量，还能挡一点 DDoS。</p><p>最后，我还是得说，祝所有 DDoS 的家伙早日被请喝茶，送一副银手镯。</p><p><img src="https://img.samhou.top/1767590744639.jpeg" alt="博客"></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>奶奶都能看懂的 C++ —— 函数重载、参数默认值与匹配</title>
      <link>https://blog.samhou.moe/cpp-overload-match/</link>
      <description>深入浅出地讲解了 C++ 中的函数重载、参数默认值与匹配规则，通过生动的代码例子和详细的优先级与匹配流程，帮助读者理解如何正确进行函数重载及避免二义性调用，同时揭示了默认参数的使用规范和限制。</description>
      <author>Sam Hou</author>
      <category domain="https://blog.samhou.moe/categories/cpp-tutorial/">C++教程</category>
      <category domain="https://blog.samhou.moe/tags/cpp/">cpp</category>
      <category domain="https://blog.samhou.moe/tags/%E5%87%BD%E6%95%B0/">函数</category>
      <category domain="https://blog.samhou.moe/tags/%E9%87%8D%E8%BD%BD/">重载</category>
      <pubDate>Fri, 02 Jan 2026 03:02:07 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p><a href="https://blog.samhou.moe/cpp-return-scope-life/">上一节</a>我们聊了返回值、作用域和对象生命周期，今天我们先来聊聊一个很方便的东西：函数重载和参数默认值，然后再来聊聊和它相关的一个重要逻辑：如何根据实参匹配对应的同名不同实现的函数？</p><p>通常来说，一个函数有一个名字，以及一个声明和定义。但是有时候，我们可能要做<strong>同一件事</strong>，但是却要根据传入的<strong>参数类型和个数</strong>，决定<strong>怎么做这件事</strong>。</p><p>这时候，就轮到<strong>函数重载</strong>出马了。</p><h2 id="快速开始：了解函数重载"><a href="#快速开始：了解函数重载" class="headerlink" title="快速开始：了解函数重载"></a>快速开始：了解函数重载</h2><p>函数重载允许我们创建<strong>相同名字</strong>的<strong>不同实现</strong>。</p><p>先来看看下面的代码。我们创建了三个版本的 addNumbers 函数，它们的参数和返回值相互不同。请先观察，然后预测一下输出：</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-function"><span class="hljs-type">int</span> <span class="hljs-title">addNumbers</span><span class="hljs-params">(<span class="hljs-type">int</span> i1, <span class="hljs-type">int</span> i2, <span class="hljs-type">int</span> i3)</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">return</span> i1 + i2 + i3;<br>&#125;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">addNumbers</span><span class="hljs-params">(<span class="hljs-type">int</span> i1, <span class="hljs-type">int</span> i2)</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">return</span> i1 + i2;<br>&#125;<br><span class="hljs-function"><span class="hljs-type">double</span> <span class="hljs-title">addNumbers</span><span class="hljs-params">(<span class="hljs-type">double</span> d1, <span class="hljs-type">double</span> d2)</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">return</span> d1 + d2;<br>&#125;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span><br><span class="hljs-function"></span>&#123;<br>    cout &lt;&lt; <span class="hljs-built_in">addNumbers</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>) &lt;&lt; endl;<br>    cout &lt;&lt; <span class="hljs-built_in">addNumbers</span>(<span class="hljs-number">1.1</span>, <span class="hljs-number">1.2</span>) &lt;&lt; endl;<br>    cout &lt;&lt; <span class="hljs-built_in">addNumbers</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>) &lt;&lt; endl;<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>输出：</p><figure class="highlight text"><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 text">3<br>2.3<br>6<br></code></pre></td></tr></table></figure><p>这里的重点是<strong>不同实现</strong>。如果我们的两种实现的参数和返回值一模一样，那么肯定会报错。这就提出来了一个核心问题：</p><p><strong>同名函数有有多个实现，怎么才能算不同呢？</strong></p><p>仔细看看这个问题。先说结论，要满足以下条件：</p><ul><li>（核心）函数参数必须有个数或者类型上的不同，即参数列表必须不同</li><li>（关于 const 修饰）两种实现之间，顶层 const 不影响其参数类型；底层 const 对参数类型有影响。也就是说，如果类型本身相同，那么必须有<strong>底层 const 上的不同</strong>，才能认为是不同参数列表。</li><li>返回值、形参名字并不影响判断。换句话说，如果只有返回值或形参的名字不同，参数列表完全一致，那么并不能算作两种不同实现。</li></ul><p>上面的代码已经足够说明判断不同的核心了：</p><ul><li>第一个和第二个，是两个 int 类型返回值的函数，参数的个数不一样，因此函数实现不同</li><li>1 和 3，2 和 3，参数的类型完全不同，因此函数实现不同。</li></ul><p>对于 const 来说，我们这里仅举一个顶层 const 的例子来说明。关于顶层和底层 const，以及具体如何判断它们，请参阅<a href="https://blog.samhou.moe/cpp-pointer-const-guide/">之前的文章</a>。</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">addNumbers</span><span class="hljs-params">(<span class="hljs-type">int</span> i1,<span class="hljs-type">const</span> <span class="hljs-type">int</span> i2)</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">return</span> i1 + i2;<br>&#125;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">addNumbers</span><span class="hljs-params">(<span class="hljs-type">int</span> i1, <span class="hljs-type">int</span> i2)</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">return</span> i1 + i2;<br>&#125;<br></code></pre></td></tr></table></figure><p>上面的代码无法通过编译，因为同名函数的多个实现仅存在顶层 const 的不同，不能算作重载，编译器会提示重复定义了一个函数。</p><p>当我们实现了不同版本的函数之后，另一个问题浮现出来了：我们调用的时候，怎么根据实参来匹配函数的不同是实现呢？</p><p>通常来说，我们可以一眼看出来，毕竟个数和类型不同。但是当类型可以随意转换时，问题就复杂了起来，这称为<strong>函数匹配</strong>，也叫<strong>重载确定</strong>。比如上面的代码：</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></pre></td><td class="code"><pre><code class="hljs cpp">cout &lt;&lt; <span class="hljs-built_in">addNumbers</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>) &lt;&lt; endl; <span class="hljs-comment">// 两个 int</span><br>cout &lt;&lt; <span class="hljs-built_in">addNumbers</span>(<span class="hljs-number">1.1</span>, <span class="hljs-number">1.2</span>) &lt;&lt; endl; <span class="hljs-comment">// 两个 double</span><br>cout &lt;&lt; <span class="hljs-built_in">addNumbers</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>) &lt;&lt; endl; <span class="hljs-comment">// 三个 int</span><br></code></pre></td></tr></table></figure><p>要想彻底理解函数匹配问题，而不是凭借感觉，我们先需要理解函数默认参数，然后再回来继续了解匹配问题。</p><h2 id="函数默认值"><a href="#函数默认值" class="headerlink" title="函数默认值"></a>函数默认值</h2><p>在 C++ 中，我们可以把一个参数设置为默认值，这样调用的时候就不需要传入了。比如：</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">addNumbers</span><span class="hljs-params">(<span class="hljs-type">int</span> i1, <span class="hljs-type">int</span> i2, <span class="hljs-type">int</span> i3 = <span class="hljs-number">5</span>)</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">return</span> i1 + i2 + i3;<br>&#125;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span><br><span class="hljs-function"></span>&#123;<br>    cout &lt;&lt; <span class="hljs-built_in">addNumbers</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>) &lt;&lt; endl; <span class="hljs-comment">// 8</span><br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>第三个参数根本没有传入，却被设定为了 8。这就是默认值的作用。</p><p>C++ 对默认值有以下规定：</p><ul><li>无论有多少，默认值<strong>必须位于末尾</strong>。如果一个参数有默认值，它本身以及它后面的参数，一定都有默认值</li><li>默认值<strong>可能影响参数匹配</strong>，但是不影响判断<strong>是否为不同的定义</strong>。比如，下面的代码是完全可以过编译的：</li></ul><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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">addNumbers</span><span class="hljs-params">(<span class="hljs-type">int</span> i1, <span class="hljs-type">int</span> i2, <span class="hljs-type">int</span> i3 = <span class="hljs-number">5</span>)</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">return</span> i1 + i2 + i3;<br>&#125;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">addNumbers</span><span class="hljs-params">(<span class="hljs-type">int</span> i1, <span class="hljs-type">int</span> i2)</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">return</span> i1 + i2;<br>&#125;<br></code></pre></td></tr></table></figure><p>虽然可以通过编译，但是你无法只传入两个实参来调用第二种定义——</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp">cout&lt;&lt;<span class="hljs-built_in">addNumbers</span>(<span class="hljs-number">1</span>,<span class="hljs-number">2</span>)&lt;&lt;endl; <span class="hljs-comment">// Error</span><br></code></pre></td></tr></table></figure><p>此外：</p><ul><li>默认值可以是作用域中<a href="https://blog.samhou.moe/cpp-return-scope-life/">处于有效区间的</a>的变量。如果是这样的话，函数在<strong>调用时</strong>会根据名字查找对应的内容</li></ul><p>比如说，下面的程序是完全合法的，不妨先猜一猜，会输出什么？</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-type">int</span> global = <span class="hljs-number">0</span>;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">addNumbers</span><span class="hljs-params">(<span class="hljs-type">int</span> i1, <span class="hljs-type">int</span> i2 = global)</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">return</span> i1 + i2;<br>&#125;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span><br><span class="hljs-function"></span>&#123;<br>    cout &lt;&lt; <span class="hljs-built_in">addNumbers</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>) &lt;&lt; endl; <span class="hljs-comment">// 没有用默认值</span><br>    cout &lt;&lt; <span class="hljs-built_in">addNumbers</span>(<span class="hljs-number">1</span>) &lt;&lt; endl; <span class="hljs-comment">// 用了一下默认值</span><br>    cout &lt;&lt; global &lt;&lt; endl; <span class="hljs-comment">// 我们之前把实参 2 复制给了形参 i2，对 global 有影响吗？有什么影响？</span><br>    global = <span class="hljs-number">4</span>; <span class="hljs-comment">// 现在 global 变成了 4，对程序又有什么影响呢？</span><br>    cout &lt;&lt; <span class="hljs-built_in">addNumbers</span>(<span class="hljs-number">1</span>) &lt;&lt; endl;<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>输出：</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-number">3</span><br><span class="hljs-number">1</span><br><span class="hljs-number">0</span><br><span class="hljs-number">5</span><br></code></pre></td></tr></table></figure><p>现在我们来详细拆解一下究竟发生了什么。</p><ul><li>首先，我们的 <code>addNumbers</code> 函数处于全局作用域中，在其定义处，是同样位于全局作用域的 global 名字的有效区间，因此，可以找到 global 这个变量</li><li>然后我们多次调用了这个函数。第一次调用没有用到默认值，将实参 2 用于初始化 <code>i2</code>，而非 global。global 仍然为 0</li><li>然后，我们用省略默认值的方式，把 global 用于初始化 <code>i2</code></li><li>之后，我们输出了 global，此时它没有被任何人修改过，因此仍然为 0</li><li>再次，我们更改了 global 的值为 4。这个时候，我们正处于名字的有效区间内，所以修改全局变量成功</li><li>之后，我们再次通过省略默认值的方式，调用了函数。调用时才会初始化形参，所以现在 i2 由 global 进行初始化，由于 global 现在是 4，所以 i2 初始化为 4，函数结果 4+1 &#x3D; 5</li></ul><p>根据上面的讲解，我们已经对默认值有了相当程度的认知，现在是时候回到那个问题了——到底是如何根据实参列表，匹配对应的函数定义的？</p><h2 id="函数匹配"><a href="#函数匹配" class="headerlink" title="函数匹配"></a>函数匹配</h2><p>C++ 针对函数匹配问题，有自己的一套流程：</p><ol><li>确定<strong>候选函数</strong></li><li>确定<strong>可行函数</strong></li><li>确定<strong>最佳匹配</strong></li><li>有单个最佳匹配吗？如果有，调用；如果没有，编译报错</li></ol><p>我们从头开始来看。</p><h3 id="确定候选函数"><a href="#确定候选函数" class="headerlink" title="确定候选函数"></a>确定候选函数</h3><p><strong>候选函数</strong>指的是，在调用处可见的函数（调用点位于函数名字的有效区间）。<br>比如，给出下面的函数声明和调用（<code>f</code> 函数定义已略去）：</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">f</span><span class="hljs-params">(<span class="hljs-type">int</span>)</span></span>; <span class="hljs-comment">// 可见</span><br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">f</span><span class="hljs-params">(<span class="hljs-type">double</span>)</span></span>; <span class="hljs-comment">// 可见</span><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>    <span class="hljs-built_in">f</span>(<span class="hljs-number">1.14</span>); <span class="hljs-comment">// 2 个候选函数</span><br>&#125;<br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">f</span><span class="hljs-params">(<span class="hljs-type">int</span>,<span class="hljs-type">int</span>)</span></span>; <span class="hljs-comment">// 不可见，不在有效区间内</span><br></code></pre></td></tr></table></figure><p>根据<a href="https://blog.samhou.moe/cpp-return-scope-life/">上一讲关于作用域的知识</a>，你应该能轻易判断出来，调用点上方的两个函数是可见的，它们即为<strong>候选函数</strong>。C++ 匹配函数时，第一步就是选出候选函数，这样就能初步确定范围。</p><h3 id="确定可行函数"><a href="#确定可行函数" class="headerlink" title="确定可行函数"></a>确定可行函数</h3><p>我们现在得到了候选函数的列表，但是究竟<strong>调用函数是否可行</strong>，这是一个问题。接下来，我们将确定<strong>可行函数</strong>，进一步缩小选择的范围。要想知道一个函数是否是调用的可行函数，要满足以下条件：</p><ul><li>所选函数的形参个数与实参个数匹配。当函数有默认值，实参个数必须位于<strong>无默认值形参数量</strong>到<strong>总形参数量</strong>之间。（简单来说，就是有默认值的形参，有没有对应的实参都无所谓）</li><li>所选函数的形参类型与实参类型匹配，或可以相互转换。</li></ul><p>比如，我们来个极端点的例子：</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">f</span><span class="hljs-params">(string s)</span></span>; <span class="hljs-comment">// 1</span><br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">f</span><span class="hljs-params">(<span class="hljs-type">int</span> i1)</span></span>; <span class="hljs-comment">// 2</span><br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">f</span><span class="hljs-params">(<span class="hljs-type">int</span> i1, <span class="hljs-type">int</span> i2 = <span class="hljs-number">3</span>)</span></span>; <span class="hljs-comment">// 3</span><br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">f</span><span class="hljs-params">(<span class="hljs-type">double</span> d1, <span class="hljs-type">double</span> d2 = <span class="hljs-number">114.514</span>)</span></span>; <span class="hljs-comment">// 4</span><br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-built_in">f</span>(<span class="hljs-number">2.1</span>); <span class="hljs-comment">// 234</span><br>    <span class="hljs-built_in">f</span>(<span class="hljs-number">2.1</span>, <span class="hljs-number">2.3</span>); <span class="hljs-comment">// 34</span><br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>上方代码中的两个调用，分别匹配了哪些候选函数、可行函数？（其实注释已经告诉你一部分答案了）</p><p>候选函数很简单，每个调用，四种函数重载都是候选，不多说。现在来确定可行函数。</p><p>先来看第一个。它只有一个参数，<code>2.1</code>，是一个 <code>double</code> 类型的实参。</p><p>首先匹配个数，1、2 两种个数都匹配。3、4 两种形参有默认值，它们可以匹配的实参个数都是 1-2 个（至少一个，最多接受两个），因此和调用都匹配。</p><p>然后匹配类型。在上面出现的类型中，实参的 <code>double</code> 类型，转换为：</p><ul><li>int，可行</li><li>string，无法转换</li></ul><p>因此，排除第一个。得出可行函数，2、3、4。</p><p>对于第二个调用，我们同样的操作，它的实参是两个 <code>double</code> 类型的参数：</p><p>先匹配个数，显然 1、2 两种只有一个形参，不匹配，排除。剩余 2、3 两种可接受 1-2 个形参，2 在范围内，匹配。</p><p>然后匹配类类型。<code>double</code> 允许转换为 <code>int</code>，全部匹配。</p><p>因此，得出可行函数，3、4。</p><p>通过这两个例子，我们已经充分说明了问题，是时候进入下一步了：确定最佳匹配。</p><h3 id="确定最佳匹配"><a href="#确定最佳匹配" class="headerlink" title="确定最佳匹配"></a>确定最佳匹配</h3><p>我们继续上面的例子，这次我们直接加上定义，让代码先跑一下，看看到底最终会调用哪一个。</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><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 cpp"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">f</span><span class="hljs-params">(string s)</span></span><br><span class="hljs-function"></span>&#123;<br>    cout &lt;&lt; <span class="hljs-string">&quot;Type1&quot;</span> &lt;&lt; endl;<br>&#125;<br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">f</span><span class="hljs-params">(<span class="hljs-type">int</span> i1)</span></span><br><span class="hljs-function"></span>&#123;<br>    cout &lt;&lt; <span class="hljs-string">&quot;Type2&quot;</span> &lt;&lt; endl;<br>&#125;<br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">f</span><span class="hljs-params">(<span class="hljs-type">int</span> i1, <span class="hljs-type">int</span> i2 = <span class="hljs-number">3</span>)</span></span><br><span class="hljs-function"></span>&#123;<br>    cout &lt;&lt; <span class="hljs-string">&quot;Type3&quot;</span> &lt;&lt; endl;<br>&#125;<br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">f</span><span class="hljs-params">(<span class="hljs-type">double</span> d1, <span class="hljs-type">double</span> d2 = <span class="hljs-number">114.514</span>)</span></span><br><span class="hljs-function"></span>&#123;<br>    cout &lt;&lt; <span class="hljs-string">&quot;Type4&quot;</span> &lt;&lt; endl;<br>&#125;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-built_in">f</span>(<span class="hljs-number">2.1</span>);<br>    <span class="hljs-built_in">f</span>(<span class="hljs-number">2.1</span>, <span class="hljs-number">2.3</span>);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>先运行以下，看看结论：</p><figure class="highlight text"><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 text">Type4<br>Type4<br></code></pre></td></tr></table></figure><p>看起来都落到了第四种重载上。这究竟是为什么，我们拆开来看。</p><p>既然要确定<strong>最佳</strong>匹配（相比于其它版本，有一个版本的函数有明显优势），我们肯定要有优先级——什么样版本的函数参数列表，和调用最相近？</p><p>先给出优先级：</p><ol><li>类型完全一致，不需要转换 &#x2F; 将实参从数组转换为指针 &#x2F; 对实参添加、移除<strong>顶层 const</strong> 修饰（这几种情况，称作<strong>精确匹配</strong>）</li><li>对底层 const 进行转换，也就是为实参添加底层 const 修饰</li><li>对实参进行类型提升转换，即把小类型，提升为大类型（如 short 到 int，char 到 int）</li><li>进行算术转换（如 int 到 double，double 到 int）</li><li>进行类转换（还没学，先记一下，类转换的优先级非常低）</li></ol><p><strong>同一优先级内没有优劣之分。</strong></p><p>要注意的一点是，第三条中，一般小的整型数会提升到 <code>int</code>，而不是 <code>short</code> 等类型。比如，<code>char</code> 类型的实参在调用时，会优先匹配 <code>char</code>，再匹配 <code>int</code>，最后是 <code>short</code>。这一点可以参考<strong>整形提升</strong>部分的规则，但这不是本文的重点，我们这里不再赘述。</p><p>我们回到例子中来。我们从可行函数列表开始，对可行函数从优先级高的开始进行选择：</p><p>首先是 <code>f(2.1)</code>： 首选 <code>f(double d1, double d2 = 114.514)</code>，因为精确匹配了 double 类型。另外几种都需要转换，因此已经找到无需转换的最佳匹配了。</p><p>其次是 <code>f(2.1,2.3)</code>：首选 <code>f(double d1, double d2 = 114.514)</code>，因为精确匹配了 double 类型。另外几种都需要转换，因此已经找到无需转换的最佳匹配了。</p><p>……上面几个有点太简单了，我们换几个调用：</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-built_in">f</span>(<span class="hljs-number">2</span>);<br><span class="hljs-built_in">f</span>(<span class="hljs-number">2</span>,<span class="hljs-number">2.1</span>);<br><span class="hljs-built_in">f</span>(<span class="hljs-number">2.1</span>,<span class="hljs-number">2</span>);<br></code></pre></td></tr></table></figure><p>你自己试一下咯。看看到底调用的是哪一个方案。</p><p>哎，如果你真的动手试过了，就会发现编译器直接报错了，根本不给你运行的机会。</p><p>为什么？这就要引出一个概念了——<strong>二义性调用</strong>。</p><h3 id="二义性调用"><a href="#二义性调用" class="headerlink" title="二义性调用"></a>二义性调用</h3><p>回到示例代码，为了让你看清楚，这次我也省略掉了定义，只保留声明，然后编号了。</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">f</span><span class="hljs-params">(string s)</span></span>; <span class="hljs-comment">// 1</span><br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">f</span><span class="hljs-params">(<span class="hljs-type">int</span> i1)</span></span>; <span class="hljs-comment">// 2</span><br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">f</span><span class="hljs-params">(<span class="hljs-type">int</span> i1, <span class="hljs-type">int</span> i2 = <span class="hljs-number">3</span>)</span></span>; <span class="hljs-comment">// 3</span><br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">f</span><span class="hljs-params">(<span class="hljs-type">double</span> d1, <span class="hljs-type">double</span> d2 = <span class="hljs-number">114.514</span>)</span></span>; <span class="hljs-comment">// 4</span><br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-built_in">f</span>(<span class="hljs-number">2</span>);<br>    <span class="hljs-built_in">f</span>(<span class="hljs-number">2</span>,<span class="hljs-number">2.1</span>);<br>    <span class="hljs-built_in">f</span>(<span class="hljs-number">2.1</span>,<span class="hljs-number">2</span>);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>候选函数四种都是。</p><p>先来看 <code>f(2)</code>，可行函数是 2、3、4。现在来确定最佳匹配：</p><ul><li>double 类型肯定没有精确匹配的 int 类型高，因此我们先把目光集中在 2、3 两种上</li><li>我们发现，2、3 全部都是精确匹配，属于<strong>同一优先级</strong>，因此<strong>没有孰优孰劣的区分</strong>（默认值是可有可无的，不会对优先级产生影响）。</li><li>因此，编译错误，哪个都<strong>没有明显优势</strong>，称为<strong>二义性调用</strong></li></ul><p>再来看 <code>f(2,2.1)</code>，可行函数是 3、4：</p><ul><li>先看第一个参数，对于 int 来说，3 是精确匹配，优先级高于算术转换的 4</li><li>再看第二个参数，对于 double 来说，4 是精确匹配，优先级高于算术转换的 3</li><li>因此，编译错误，哪个都<strong>没有明显优势</strong>，存在<strong>二义性调用</strong></li></ul><p>对于 <code>f(2.1,2)</code>，它和前一个几乎一模一样，因此不再赘述，你可以自己尝试分析一下。</p><p>由此我们可以得出结论，编译器默认会选择匹配程度最高的方案，但当<strong>谁都没有明显优势</strong>的时候，<strong>编译器会报错</strong>。</p><p>为了澄清到底什么是<strong>明显优势</strong>（一种实现的形参列表明显与实参列表更加相似），彻底搞清楚什么时候会产生二义性调用，我们先把参数个数升级到三个，然后再揭示规则：</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></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">f</span><span class="hljs-params">(<span class="hljs-type">int</span> i1, <span class="hljs-type">int</span> i2 = <span class="hljs-number">3</span>, <span class="hljs-type">int</span> i3 = <span class="hljs-number">5</span>)</span></span>;<br><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">f</span><span class="hljs-params">(<span class="hljs-type">double</span> d1, <span class="hljs-type">double</span> d2 = <span class="hljs-number">114.514</span>, <span class="hljs-type">double</span> d3 = <span class="hljs-number">114.514</span>)</span></span>;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-built_in">f</span>(<span class="hljs-number">2</span>, <span class="hljs-number">2.1</span>, <span class="hljs-number">2.2</span>);<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>先猜猜看，上面的代码能不能通过编译呢？从直觉上来看，似乎可以——有两个可行函数，第二个精确匹配的个数更多，所以调用第二个。</p><p>但实际上你自己试一下，就会发现仍然报错。</p><p>为什么？我们现在给出，一个版本的函数实现 A 优于另一个实现 B 的<strong>绝对优势判断准则</strong>，必须满足以下两条（小提示，请回到上一小节，参考相似优先级列表）：</p><ul><li>A 的<strong>所有参数</strong>，相比于 B，匹配程度（参数类型相似程度）都<strong>不差</strong>（相等或更加相似）</li><li>A 的<strong>至少一个参数</strong>，相比于 B，匹配程度<strong>高</strong></li></ul><p>现在再来看例子，试着分析一下绝对优势：</p><p>调用点有三个参数，第一个是 int，第二、第三个是 double 类型。</p><p>先看第一个参数，对第一种的函数是精确匹配，优于第二种。再看后面两个参数，对第二种的函数是精确匹配，优于第一种。</p><p>现在套用判断准则。</p><p>1 优于 2 吗？</p><ul><li>1 存在第二、第三个参数比 2 匹配程度差，不符合第一条</li><li>1 有第一个参数优于 2，符合第二条</li></ul><p>由于必须满足两条，因此不成立，1 不优于 2。</p><p>2 优于 1 吗？</p><ul><li>2 存在第一个参数比 1 匹配程度差，不符合第一条</li><li>2 有第二、第三个参数优于 1，符合第二条</li></ul><p>同理，不成立，2 不优于 1。</p><p>因此，谁也没有绝对优势，存在二义性。</p><p>经过上面的讲解，我们已经对函数重载规则、匹配方式有了相当程度的认知。本节的重点是不同实现是否有区别的判断、默认值放在末尾的规范、匹配的方法和优先级，每一个都进行了深入的讲解。下一节，我们将把函数与指针结合起来，先讲解函数指针，然后再聊聊指针的高级用法。</p>]]>
      </content:encoded>
    </item>
  </channel>
</rss>
