C# 14 无疑是一个令人翘首以盼的版本,它带来了许多新特性和改进,旨在让我们的编程工作更加高效和便捷。官方公布的新特性列表相当丰富,包括:

  • 扩展成员 (Extension members)
  • 空条件赋值 (Null-conditional assignments)
  • nameof 支持未绑定泛型类型 (nameof with unbound generic types)
  • Span<T>ReadOnlySpan<T> 提供更多隐式转换 (More implicit conversions for Span<T> and ReadOnlySpan<T>)
  • 简单 lambda 参数上的修饰符 (Modifiers on simple lambda parameters)
  • field 支持的属性 (field-backed properties)
  • 分部事件和构造函数 (Partial events and constructors)
  • 用户定义的复合赋值运算符 (User-defined compound assignment operators)

在众多闪亮的新特性中,我个人最钟情的是这最后一位——用户定义的复合赋值运算符。这个名字听起来可能有些拗口,但它所代表的功能却非常直观,其实就是允许我们为 ++, --, +=, -=, *=, /= 等运算符编写自定义的重载版本。

为什么我们需要重载 += 这类运算符?

对于像我这样有 C++ 背景的开发者来说,这简直是“刚需”,甚至是当初从 C++ 转向 C# 时最先感到不适的痛点之一(当然,转到 Java 后的不适感会更明显——这里小小调侃一下)。

C++ 的运算符重载同时支持实例级别和静态级别,而 C# 14 之前的版本只支持静态级别的运算符重载。这意味着在 C# 中,我们可以重载 +-,却无法直接定义 +=-= 的行为。

我之前还在 Stack Overflow 上深入研究过这个问题,在一个题为 "Why it is not possible to overload compound assignment operator in C#?" 的帖子里,讨论非常有意思。大多数人的观点是:x += y 完全等价于 x = x + y,它仅仅是一个语法糖,因此没有必要专门为它提供重载支持,还有人专门论证为什么 C# 不需要这样的功能,令人困惑。

然而,对于我们这些写过 C++ 的人来说,它并不仅仅是语法糖那么简单。我至今还记得大学时用 C++ 实现的一个简单矩阵类,其核心操作大致如下:

class Matrix
{
public:
Matrix(int rows, int cols) : rows(rows), cols(cols) {
data = new int[rows * cols];
}
~Matrix() {
delete[] data;
} // 实例级别的复合赋值运算符重载
Matrix& operator+=(const Matrix& other) {
for (int i = 0; i < rows * cols; ++i) {
data[i] += other.data[i];
}
return *this;
}
private:
int rows, cols;
int* data;
};

在 C# 14 之前,为了实现类似的功能,我们只能这样做:

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[rows * cols];
} // 静态级别的二元运算符重载
public static Matrix operator +(Matrix x, Matrix y)
{
// 注意:这里的实现为了简化,直接修改了 x 的内容并返回
// 更规范的实现会创建一个新的 Matrix 实例
for (int i = 0; i < x.rows * x.cols; ++i)
{
x.data[i] += y.data[i];
}
return x;
}
}

你能看出这两者之间那个非常、非常重要的区别吗?

x += y 这个操作中,C# 的 operator+ 会隐式地创建一个临时对象。整个过程是:

  1. 调用 operator+ 计算 x + y 的结果,生成一个全新的对象。
  2. 将这个新对象的引用赋值给 x
  3. 原来的 x 所引用的对象如果没有其他引用,则会被垃圾回收器回收。

而在 C++ 的例子中,operator+=直接在原有对象上进行修改,不会产生任何新的对象。这种“就地操作”的方式,效率显然更高。

不仅仅是性能,更是资源管理的命脉

如果说这一点小小的性能差异还不足以打动你,那么接下来的问题则更为致命,尤其是当你的类需要管理非托管资源时。让我们看看实现了 IDisposable 接口的 Matrix 类:

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;
}
}

在这种情况下,m1 += m2; 这行代码背后发生的 m1 = m1 + m2; 将会是一场灾难。m1 + m2 创建的那个临时 Matrix 对象,它内部也分配了非托管内存。但我们无法获取到这个临时对象的引用来调用它的 Dispose 方法!

这意味着我们只能依赖 Finalizer (终结器) 来回收这部分非托管内存。这会导致资源被占用的时间不可控,增加了内存泄漏的风险,并给GC带来了不必要的压力。很不幸,我在自己的开源项目 Sdcb.Arithmetic 中就曾直面这个问题。当时 C# 14 尚未发布,我不得不为所有类似 GmpIntegerGmpFloat 的类都加上 Finalizer 来处理临时对象可能导致的内存泄漏。

一个带有 Finalizer 的实现大概是这样:

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+ 的实现会创建新对象,其资源回收依赖终结器
// ...
}

这种被动的资源管理方式,既不优雅,也暗藏风险。

C# 14 的优雅解决方案

然而,这一切的挣扎和妥协,随着 C# 14 的到来而画上了句号。最好的解决方案终于出现了——用户定义的复合赋值运算符。它允许我们避免创建临时对象,直接在实例上进行操作,从而同时解决了性能和资源管理两大难题。

现在,我们可以这样编写我们的 Matrix 类:

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[rows * cols];
} 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 < result.rows * result.cols; ++i)
{
result.data[i] = left.data[i] + right.data[i];
}
return result;
} // C# 14 新特性:实例级别的 operator+=,直接修改当前对象
public void operator +=(Matrix right)
{
for (int i = 0; i < rows * cols; ++i)
{
data[i] += right.data[i];
}
}
}

你可能已经注意到了几个关键点:

  1. 新的 operator+= 是一个实例方法public void),而不是静态方法,这与 C++ 的行为完全一致。
  2. 它与静态的 operator+ 可以共存。编译器会根据上下文智能选择:当执行 a += b; 时,会优先调用实例的 operator+=;当执行 var c = a + b; 时,则会调用静态的 operator+
  3. operator+= 直接修改当前对象的数据,而 operator+ 则是返回一个全新的对象。二者的实现逻辑可以完全不同,提供了极高的灵活性。

现在,你可以放心地编写如下代码,它既简洁又高效,完美地利用了 C# 14 的新特性:

Matrix a = new Matrix(2, 2);
Matrix b = new Matrix(2, 2); // 调用实例方法 operator+=,无临时对象,无性能损耗,无资源风险
a += b;

总结

C# 14 引入的用户定义的复合赋值运算符,远不止是一个语法糖。它解决了 C# 长期以来在运算符重载方面的一个核心痛点,特别是在处理需要精细化管理的资源(如非托管内存、文件句柄等)时。

这个新特性带来了两大好处:

  1. 性能提升:通过“就地修改”避免了不必要的临时对象分配和垃圾回收开销。
  2. 安全性增强:从根本上消除了因临时对象而导致的资源泄漏风险,让我们不再需要依赖于不可预测的终结器来进行补救。

它使得 C# 在高性能和底层交互编程方面更加得心应手,也让我们这些有 C++ 背景的开发者感到无比亲切。这无疑是我在 C# 14 中最欣赏、也是最实用的一个改进。


感谢阅读到这里,如果感觉本文对您有帮助,请不吝评论点赞,这也是我持续创作的动力!

也欢迎加入我的 .NET骚操作 QQ群:495782587,一起交流.NET 和 AI 的各种有趣玩法!

我最喜欢的 C# 14 新特性的更多相关文章

  1. C++ 14 新特性总结

    转载自: http://www.codeceo.com/article/cpp-14-new-features.html C++14 这一继C++11 之后的新的 C++ 标准已经被正式批准,正在向 ...

  2. c++14新特性

    1.函数返回值类型推导 c++14对函数返回类型推导规则做了优化: auto func(int i) { //C++11编译非法,c++14支持auto返回值类型推导 return i; } int ...

  3. Java 15 正式发布, 14 个新特性,刷新你的认知!!

    JDK 15 2020/09/15 如期而至! 这个时间牛逼啊,和苹果发布会同天? OracleJDK 15 发布地址: https://www.oracle.com/java/technologie ...

  4. Java程序员必备基础:JDK 5-15都有哪些经典新特性

    前言 JDK 15发布啦~ 我们一起回顾JDK 5-15 的新特性吧,大家一起学习哈~ 本文已经收录到github ❝ https://github.com/whx123/JavaHome ❞ 「公众 ...

  5. Java9至17的新特性总结

    总览 讲讲Java 9-17 的一些语法糖和一些新发布的jeps, 重点讲讲JVM的垃圾回收器 时间线 SpringBoot 为什么选择Java17这个版本.我估计跟下面这个图有关系. Java 8 ...

  6. Java 17 新特性:switch的模式匹配(Preview)

    还记得Java 16中的instanceof增强吗? 通过下面这个例子再回忆一下: Map<String, Object> data = new HashMap<>(); da ...

  7. atitit.eclipse 新特性总结3.1--4.3

    atitit.eclipse 新特性总结3.1--4.3 Eclipse 3.1 1 Eclipse 3.2 Java开发工具的新特性 2 1. 内容辅助(Ctrl+Space)模板 2 2. 动态地 ...

  8. 转:关于C++14:你需要知道的新特性

    关于C++14:你需要知道的新特性 遇见C++ Lambda C++14 lambda 教程 C++11 lambda表达式 C++标准库:使用 std::for_each std::generate ...

  9. 我最喜欢的visual studio 2013的新特性

    博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:我最喜欢的visual studio 2013的新特性.

  10. C++11 & C++14 & C++17新特性

    C++11:C++11包括大量的新特性:包括lambda表达式,类型推导关键字auto.decltype,和模板的大量改进. 新的关键字 auto C++11中引入auto第一种作用是为了自动类型推导 ...

随机推荐

  1. JAVA的那些数据结构实现总结,实现,扩容说明

    能沉淀下来的东西,往往都很基础,整理了下JAVA中遇到的数据结构 目录大纲: 到目前接触到的 有几个说明: 可扩容数组 ArrayList 扩容数组的实现, 满了后扩容,扩容在1.5倍,通过copy过 ...

  2. #ifndef 、 #define 、#endif使用解释

    在C语言程序代码里,看到了这么一段代码: #ifndef __WIFI_CONNECT_H_ #define __WIFI_CONNECT_H_ int WifiConnect(const char ...

  3. 50道常见Redis面试题,干货汇总

      哪些大厂在使用Redis?github.twitter.微博.Stack Overflow.百度.阿里巴巴.美团和搜狐等都在用,所以今天小编当作搬运工,为大家整理了一份Redis面试题,合计50个 ...

  4. RabbitMq安装、配置

    #安装 apt install rabbitmq #启动 rabbitmqctl start_app #查看状态 rabbitmqctl status #退出 rabbitmqctl stop #gu ...

  5. 敏捷史话(十):我牺牲了滑雪时间,参加了一场软件革命——Jon Kern

    "在镜头定格的一刹那,所有美好都和你不期而遇",这是 Jon Kern 对生活的表达.为了更好地记录生活,他在一家名为 flickr 的网站上创建了一个属于自己的照片博客,在这个博 ...

  6. 博创Luby使用指南

    Luby使用指南 1.开机 通电,当显示在boot界面的时候,长按正方形(深灰色)那个键,即可进入选择程序界面,此时再按一次正方形那个键,即可进入USB连接模式,此时用线将Luby和电脑连接起来. 当 ...

  7. Java IO<3>处理流:缓冲流 数据流 转换流 对象流

    Java io 处理流 节点流和处理流概述 Java流可以分节点流和处理流两类. 节点流是面向各种物理节点的流,比如面向读写文件的FileInputStream和FileOutputStream:面向 ...

  8. UPS 6航班空难分析与总结

    1. 事件概况 事故时间:2008年9月3日 事故地点:阿联酋迪拜国际机场附近 机型:波音747-400F(货运型) 航班编号:UPS 6 机组成员:2人(1名机长和1名副驾驶) 机上人员伤亡:机组人 ...

  9. 多重集r-组合数与组合方案

    多重集的r-组合是非常常见的组合问题, 但相关资料通常只给出组合数的计算, 却无法给出实际的方案, 下面将通过一个水果摆盘问题由简单到复杂逐步推导并给出最终的求组合数和组合方案的算法. 水果拼盘问题 ...

  10. AAAI 2025-FEI: 频率掩码嵌入推理:一种非对比学习的时间序列表示学习

    title:Frequency-Masked Embedding Inference: A Non-Contrastive Approach for Time Series Representatio ...