话费折扣充值 發表於 2025-8-6 08:45:00

我最喜欢的 C# 14 新特性

<p>C# 14 无疑是一个令人翘首以盼的版本,它带来了许多新特性和改进,旨在让我们的编程工作更加高效和便捷。官方公布的新特性列表相当丰富,包括:</p>
<ul>
<li>扩展成员 (Extension members)</li>
<li>空条件赋值 (Null-conditional assignments)</li>
<li><code>nameof</code> 支持未绑定泛型类型 (nameof with unbound generic types)</li>
<li>为 <code>Span&lt;T&gt;</code> 和 <code>ReadOnlySpan&lt;T&gt;</code> 提供更多隐式转换 (More implicit conversions for <code>Span&lt;T&gt;</code> and <code>ReadOnlySpan&lt;T&gt;</code>)</li>
<li>简单 lambda 参数上的修饰符 (Modifiers on simple lambda parameters)</li>
<li><code>field</code> 支持的属性 (<code>field</code>-backed properties)</li>
<li>分部事件和构造函数 (Partial events and constructors)</li>
<li><strong>用户定义的复合赋值运算符 (User-defined compound assignment operators)</strong></li>
</ul>
<p>在众多闪亮的新特性中,我个人最钟情的是这最后一位——<strong>用户定义的复合赋值运算符</strong>。这个名字听起来可能有些拗口,但它所代表的功能却非常直观,其实就是允许我们为 <code>++</code>, <code>--</code>, <code>+=</code>, <code>-=</code>, <code>*=</code>, <code>/=</code> 等运算符编写自定义的重载版本。</p>
<p><img src="https://img2024.cnblogs.com/blog/233608/202508/233608-20250805230610231-1915548986.png" alt="image" loading="lazy"></p>
<h2 id="为什么我们需要重载--这类运算符">为什么我们需要重载 <code>+=</code> 这类运算符?</h2>
<p>对于像我这样有 C++ 背景的开发者来说,这简直是“刚需”,甚至是当初从 C++ 转向 C# 时最先感到不适的痛点之一(当然,转到 Java 后的不适感会更明显——这里小小调侃一下)。</p>
<p>C++ 的运算符重载同时支持实例级别和静态级别,而 C# 14 之前的版本只支持静态级别的运算符重载。这意味着在 C# 中,我们可以重载 <code>+</code> 和 <code>-</code>,却无法直接定义 <code>+=</code> 和 <code>-=</code> 的行为。</p>
<p>我之前还在 Stack Overflow 上深入研究过这个问题,在一个题为 "Why it is not possible to overload compound assignment operator in C#?" 的帖子里,讨论非常有意思。大多数人的观点是:<code>x += y</code> 完全等价于 <code>x = x + y</code>,它仅仅是一个语法糖,因此没有必要专门为它提供重载支持,还有人专门论证为什么 C# 不需要这样的功能,令人困惑。</p>
<p>然而,对于我们这些写过 C++ 的人来说,它<strong>并不仅仅是语法糖</strong>那么简单。我至今还记得大学时用 C++ 实现的一个简单矩阵类,其核心操作大致如下:</p>
<pre><code class="language-cpp">class Matrix
{
public:
    Matrix(int rows, int cols) : rows(rows), cols(cols) {
      data = new int;
    }
    ~Matrix() {
      delete[] data;
    }

    // 实例级别的复合赋值运算符重载
    Matrix&amp; operator+=(const Matrix&amp; other) {
      for (int i = 0; i &lt; rows * cols; ++i) {
            data += other.data;
      }
      return *this;
    }
private:
    int rows, cols;
    int* data;
};
</code></pre>
<p>在 C# 14 之前,为了实现类似的功能,我们只能这样做:</p>
<pre><code class="language-csharp">public class Matrix
{
    private int rows;
    private int cols;
    private int[] data;

    public Matrix(int rows, int cols)
    {
      this.rows = rows;
      this.cols = cols;
      this.data = new int;
    }

    // 静态级别的二元运算符重载
    public static Matrix operator +(Matrix x, Matrix y)
    {
      // 注意:这里的实现为了简化,直接修改了 x 的内容并返回
      // 更规范的实现会创建一个新的 Matrix 实例
      for (int i = 0; i &lt; x.rows * x.cols; ++i)
      {
            x.data += y.data;
      }
      return x;
    }
}
</code></pre>
<p>你能看出这两者之间那个<strong>非常、非常重要</strong>的区别吗?</p>
<p>在 <code>x += y</code> 这个操作中,C# 的 <code>operator+</code> 会隐式地创建一个<strong>临时对象</strong>。整个过程是:</p>
<ol>
<li>调用 <code>operator+</code> 计算 <code>x + y</code> 的结果,生成一个全新的对象。</li>
<li>将这个新对象的引用赋值给 <code>x</code>。</li>
<li>原来的 <code>x</code> 所引用的对象如果没有其他引用,则会被垃圾回收器回收。</li>
</ol>
<p>而在 C++ 的例子中,<code>operator+=</code> 是<strong>直接在原有对象上进行修改</strong>,不会产生任何新的对象。这种“就地操作”的方式,效率显然更高。</p>
<h2 id="不仅仅是性能更是资源管理的命脉">不仅仅是性能,更是资源管理的命脉</h2>
<p>如果说这一点小小的性能差异还不足以打动你,那么接下来的问题则更为致命,尤其是当你的类需要管理非托管资源时。让我们看看实现了 <code>IDisposable</code> 接口的 <code>Matrix</code> 类:</p>
<pre><code class="language-csharp">public class Matrix : IDisposable
{
    private int rows;
    private int cols;
    private IntPtr data; // 使用 Marshal 分配的非托管内存

    public Matrix(int rows, int cols)
    {
      this.rows = rows;
      this.cols = cols;
      // 分配非托管内存
      this.data = Marshal.AllocHGlobal(rows * cols * sizeof(int));
    }

    public void Dispose()
    {
      Marshal.FreeHGlobal(data);
    }

    public static Matrix operator +(Matrix x, Matrix y)
    {
      var result = new Matrix(x.rows, x.cols); // 必须创建一个新对象来存放结果
      // ... 执行加法操作 ...
      return result;
    }
}
</code></pre>
<p>在这种情况下,<code>m1 += m2;</code> 这行代码背后发生的 <code>m1 = m1 + m2;</code> 将会是一场灾难。<code>m1 + m2</code> 创建的那个<strong>临时 <code>Matrix</code> 对象</strong>,它内部也分配了非托管内存。但我们无法获取到这个临时对象的引用来调用它的 <code>Dispose</code> 方法!</p>
<p>这意味着我们只能依赖 <strong>Finalizer (终结器)</strong> 来回收这部分非托管内存。这会导致资源被占用的时间不可控,增加了内存泄漏的风险,并给GC带来了不必要的压力。很不幸,我在自己的开源项目 <code>Sdcb.Arithmetic</code> 中就曾直面这个问题。当时 C# 14 尚未发布,我不得不为所有类似 <code>GmpInteger</code> 和 <code>GmpFloat</code> 的类都加上 Finalizer 来处理临时对象可能导致的内存泄漏。</p>
<p>一个带有 Finalizer 的实现大概是这样:</p>
<pre><code class="language-csharp">public unsafe class Matrix : IDisposable
{
    private IntPtr data;
    // ... 其他成员 ...

    public Matrix(int rows, int cols)
    {
      this.data = Marshal.AllocHGlobal(rows * cols * sizeof(int));
    }

    public void Dispose()
    {
      Dispose(true);
      GC.SuppressFinalize(this); // 通知 GC 不再需要调用终结器
    }

    protected virtual void Dispose(bool disposing)
    {
      if (data != IntPtr.Zero)
      {
            Marshal.FreeHGlobal(data);
            data = IntPtr.Zero;
      }
    }

    ~Matrix() // 终结器
    {
      Dispose(false);
    }
   
    // operator+ 的实现会创建新对象,其资源回收依赖终结器
    // ...
}
</code></pre>
<p>这种被动的资源管理方式,既不优雅,也暗藏风险。</p>
<h2 id="c-14-的优雅解决方案">C# 14 的优雅解决方案</h2>
<p>然而,这一切的挣扎和妥协,随着 C# 14 的到来而画上了句号。最好的解决方案终于出现了——<strong>用户定义的复合赋值运算符</strong>。它允许我们避免创建临时对象,直接在实例上进行操作,从而同时解决了性能和资源管理两大难题。</p>
<p>现在,我们可以这样编写我们的 <code>Matrix</code> 类:</p>
<pre><code class="language-csharp">public class Matrix : IDisposable
{
    private int rows;
    private int cols;
    private int[] data; // 为了简化,这里用回托管数组

    public Matrix(int rows, int cols)
    {
      this.rows = rows;
      this.cols = cols;
      this.data = new int;
    }
   
    public void Dispose() { /* ... */ }

    // 经典的静态 operator+,返回一个新对象,用于 a = b + c 的场景
    public static Matrix operator +(Matrix left, Matrix right)
    {
      var result = new Matrix(left.rows, left.cols);
      for (int i = 0; i &lt; result.rows * result.cols; ++i)
      {
            result.data = left.data + right.data;
      }
      return result;
    }

    // C# 14 新特性:实例级别的 operator+=,直接修改当前对象
    public void operator +=(Matrix right)
    {
      for (int i = 0; i &lt; rows * cols; ++i)
      {
            data += right.data;
      }
    }
}
</code></pre>
<p>你可能已经注意到了几个关键点:</p>
<ol>
<li>新的 <code>operator+=</code> 是一个<strong>实例方法</strong>(<code>public void</code>),而不是静态方法,这与 C++ 的行为完全一致。</li>
<li>它与静态的 <code>operator+</code> 可以<strong>共存</strong>。编译器会根据上下文智能选择:当执行 <code>a += b;</code> 时,会优先调用实例的 <code>operator+=</code>;当执行 <code>var c = a + b;</code> 时,则会调用静态的 <code>operator+</code>。</li>
<li><code>operator+=</code> 直接修改当前对象的数据,而 <code>operator+</code> 则是返回一个全新的对象。二者的实现逻辑可以完全不同,提供了极高的灵活性。</li>
</ol>
<p>现在,你可以放心地编写如下代码,它既简洁又高效,完美地利用了 C# 14 的新特性:</p>
<pre><code class="language-csharp">Matrix a = new Matrix(2, 2);
Matrix b = new Matrix(2, 2);

// 调用实例方法 operator+=,无临时对象,无性能损耗,无资源风险
a += b;
</code></pre>
<h2 id="总结">总结</h2>
<p>C# 14 引入的<strong>用户定义的复合赋值运算符</strong>,远不止是一个语法糖。它解决了 C# 长期以来在运算符重载方面的一个核心痛点,特别是在处理需要精细化管理的资源(如非托管内存、文件句柄等)时。</p>
<p>这个新特性带来了两大好处:</p>
<ol>
<li><strong>性能提升</strong>:通过“就地修改”避免了不必要的临时对象分配和垃圾回收开销。</li>
<li><strong>安全性增强</strong>:从根本上消除了因临时对象而导致的资源泄漏风险,让我们不再需要依赖于不可预测的终结器来进行补救。</li>
</ol>
<p>它使得 C# 在高性能和底层交互编程方面更加得心应手,也让我们这些有 C++ 背景的开发者感到无比亲切。这无疑是我在 C# 14 中最欣赏、也是最实用的一个改进。</p>
<hr>
<p>感谢阅读到这里,如果感觉本文对您有帮助,请不吝<strong>评论</strong>和<strong>点赞</strong>,这也是我持续创作的动力!<br>
也欢迎加入我的 <strong>.NET骚操作 QQ群:495782587</strong>,一起交流.NET 和 AI 的各种有趣玩法!</p><br><br>
来源:https://www.cnblogs.com/sdcb/p/19024248/my-favorit-csharp-14-feature
頁: [1]
查看完整版本: 我最喜欢的 C# 14 新特性