我最喜歡的 C# 14 新特性
C# 14 無疑是一個令人翹首以盼的版本,它帶來了許多新特性和改進(jìn),旨在讓我們的編程工作更加高效和便捷。官方公布的新特性列表相當(dāng)豐富,包括:
- 擴(kuò)展成員 (Extension members)
- 空條件賦值 (Null-conditional assignments)
nameof支持未綁定泛型類型 (nameof with unbound generic types)- 為
Span<T>和ReadOnlySpan<T>提供更多隱式轉(zhuǎn)換 (More implicit conversions forSpan<T>andReadOnlySpan<T>) - 簡單 lambda 參數(shù)上的修飾符 (Modifiers on simple lambda parameters)
field支持的屬性 (field-backed properties)- 分部事件和構(gòu)造函數(shù) (Partial events and constructors)
- 用戶定義的復(fù)合賦值運(yùn)算符 (User-defined compound assignment operators)
在眾多閃亮的新特性中,我個人最鐘情的是這最后一位——用戶定義的復(fù)合賦值運(yùn)算符。這個名字聽起來可能有些拗口,但它所代表的功能卻非常直觀,其實(shí)就是允許我們?yōu)?++, --, +=, -=, *=, /= 等運(yùn)算符編寫自定義的重載版本。

為什么我們需要重載 += 這類運(yùn)算符?
對于像我這樣有 C++ 背景的開發(fā)者來說,這簡直是“剛需”,甚至是當(dāng)初從 C++ 轉(zhuǎn)向 C# 時最先感到不適的痛點(diǎn)之一(當(dāng)然,轉(zhuǎn)到 Java 后的不適感會更明顯——這里小小調(diào)侃一下)。
C++ 的運(yùn)算符重載同時支持實(shí)例級別和靜態(tài)級別,而 C# 14 之前的版本只支持靜態(tài)級別的運(yùn)算符重載。這意味著在 C# 中,我們可以重載 + 和 -,卻無法直接定義 += 和 -= 的行為。
我之前還在 Stack Overflow 上深入研究過這個問題,在一個題為 "Why it is not possible to overload compound assignment operator in C#?" 的帖子里,討論非常有意思。大多數(shù)人的觀點(diǎn)是:x += y 完全等價于 x = x + y,它僅僅是一個語法糖,因此沒有必要專門為它提供重載支持,還有人專門論證為什么 C# 不需要這樣的功能,令人困惑。
然而,對于我們這些寫過 C++ 的人來說,它并不僅僅是語法糖那么簡單。我至今還記得大學(xué)時用 C++ 實(shí)現(xiàn)的一個簡單矩陣類,其核心操作大致如下:
class Matrix
{
public:
Matrix(int rows, int cols) : rows(rows), cols(cols) {
data = new int[rows * cols];
}
~Matrix() {
delete[] data;
}
// 實(shí)例級別的復(fù)合賦值運(yùn)算符重載
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 之前,為了實(shí)現(xiàn)類似的功能,我們只能這樣做:
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];
}
// 靜態(tài)級別的二元運(yùn)算符重載
public static Matrix operator +(Matrix x, Matrix y)
{
// 注意:這里的實(shí)現(xiàn)為了簡化,直接修改了 x 的內(nèi)容并返回
// 更規(guī)范的實(shí)現(xiàn)會創(chuàng)建一個新的 Matrix 實(shí)例
for (int i = 0; i < x.rows * x.cols; ++i)
{
x.data[i] += y.data[i];
}
return x;
}
}
你能看出這兩者之間那個非常、非常重要的區(qū)別嗎?
在 x += y 這個操作中,C# 的 operator+ 會隱式地創(chuàng)建一個臨時對象。整個過程是:
- 調(diào)用
operator+計算x + y的結(jié)果,生成一個全新的對象。 - 將這個新對象的引用賦值給
x。 - 原來的
x所引用的對象如果沒有其他引用,則會被垃圾回收器回收。
而在 C++ 的例子中,operator+= 是直接在原有對象上進(jìn)行修改,不會產(chǎn)生任何新的對象。這種“就地操作”的方式,效率顯然更高。
不僅僅是性能,更是資源管理的命脈
如果說這一點(diǎn)小小的性能差異還不足以打動你,那么接下來的問題則更為致命,尤其是當(dāng)你的類需要管理非托管資源時。讓我們看看實(shí)現(xiàn)了 IDisposable 接口的 Matrix 類:
public class Matrix : IDisposable
{
private int rows;
private int cols;
private IntPtr data; // 使用 Marshal 分配的非托管內(nèi)存
public Matrix(int rows, int cols)
{
this.rows = rows;
this.cols = cols;
// 分配非托管內(nèi)存
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); // 必須創(chuàng)建一個新對象來存放結(jié)果
// ... 執(zhí)行加法操作 ...
return result;
}
}
在這種情況下,m1 += m2; 這行代碼背后發(fā)生的 m1 = m1 + m2; 將會是一場災(zāi)難。m1 + m2 創(chuàng)建的那個臨時 Matrix 對象,它內(nèi)部也分配了非托管內(nèi)存。但我們無法獲取到這個臨時對象的引用來調(diào)用它的 Dispose 方法!
這意味著我們只能依賴 Finalizer (終結(jié)器) 來回收這部分非托管內(nèi)存。這會導(dǎo)致資源被占用的時間不可控,增加了內(nèi)存泄漏的風(fēng)險,并給GC帶來了不必要的壓力。很不幸,我在自己的開源項(xiàng)目 Sdcb.Arithmetic 中就曾直面這個問題。當(dāng)時 C# 14 尚未發(fā)布,我不得不為所有類似 GmpInteger 和 GmpFloat 的類都加上 Finalizer 來處理臨時對象可能導(dǎo)致的內(nèi)存泄漏。
一個帶有 Finalizer 的實(shí)現(xiàn)大概是這樣:
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 不再需要調(diào)用終結(jié)器
}
protected virtual void Dispose(bool disposing)
{
if (data != IntPtr.Zero)
{
Marshal.FreeHGlobal(data);
data = IntPtr.Zero;
}
}
~Matrix() // 終結(jié)器
{
Dispose(false);
}
// operator+ 的實(shí)現(xiàn)會創(chuàng)建新對象,其資源回收依賴終結(jié)器
// ...
}
這種被動的資源管理方式,既不優(yōu)雅,也暗藏風(fēng)險。
C# 14 的優(yōu)雅解決方案
然而,這一切的掙扎和妥協(xié),隨著 C# 14 的到來而畫上了句號。最好的解決方案終于出現(xiàn)了——用戶定義的復(fù)合賦值運(yùn)算符。它允許我們避免創(chuàng)建臨時對象,直接在實(shí)例上進(jìn)行操作,從而同時解決了性能和資源管理兩大難題。
現(xiàn)在,我們可以這樣編寫我們的 Matrix 類:
public class Matrix : IDisposable
{
private int rows;
private int cols;
private int[] data; // 為了簡化,這里用回托管數(shù)組
public Matrix(int rows, int cols)
{
this.rows = rows;
this.cols = cols;
this.data = new int[rows * cols];
}
public void Dispose() { /* ... */ }
// 經(jīng)典的靜態(tài) 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 新特性:實(shí)例級別的 operator+=,直接修改當(dāng)前對象
public void operator +=(Matrix right)
{
for (int i = 0; i < rows * cols; ++i)
{
data[i] += right.data[i];
}
}
}
你可能已經(jīng)注意到了幾個關(guān)鍵點(diǎn):
- 新的
operator+=是一個實(shí)例方法(public void),而不是靜態(tài)方法,這與 C++ 的行為完全一致。 - 它與靜態(tài)的
operator+可以共存。編譯器會根據(jù)上下文智能選擇:當(dāng)執(zhí)行a += b;時,會優(yōu)先調(diào)用實(shí)例的operator+=;當(dāng)執(zhí)行var c = a + b;時,則會調(diào)用靜態(tài)的operator+。 operator+=直接修改當(dāng)前對象的數(shù)據(jù),而operator+則是返回一個全新的對象。二者的實(shí)現(xiàn)邏輯可以完全不同,提供了極高的靈活性。
現(xiàn)在,你可以放心地編寫如下代碼,它既簡潔又高效,完美地利用了 C# 14 的新特性:
Matrix a = new Matrix(2, 2);
Matrix b = new Matrix(2, 2);
// 調(diào)用實(shí)例方法 operator+=,無臨時對象,無性能損耗,無資源風(fēng)險
a += b;
總結(jié)
C# 14 引入的用戶定義的復(fù)合賦值運(yùn)算符,遠(yuǎn)不止是一個語法糖。它解決了 C# 長期以來在運(yùn)算符重載方面的一個核心痛點(diǎn),特別是在處理需要精細(xì)化管理的資源(如非托管內(nèi)存、文件句柄等)時。
這個新特性帶來了兩大好處:
- 性能提升:通過“就地修改”避免了不必要的臨時對象分配和垃圾回收開銷。
- 安全性增強(qiáng):從根本上消除了因臨時對象而導(dǎo)致的資源泄漏風(fēng)險,讓我們不再需要依賴于不可預(yù)測的終結(jié)器來進(jìn)行補(bǔ)救。
它使得 C# 在高性能和底層交互編程方面更加得心應(yīng)手,也讓我們這些有 C++ 背景的開發(fā)者感到無比親切。這無疑是我在 C# 14 中最欣賞、也是最實(shí)用的一個改進(jìn)。
感謝閱讀到這里,如果感覺本文對您有幫助,請不吝評論和點(diǎn)贊,這也是我持續(xù)創(chuàng)作的動力!
也歡迎加入我的 .NET騷操作 QQ群:495782587,一起交流.NET 和 AI 的各種有趣玩法!

浙公網(wǎng)安備 33010602011771號