「Equals 和 == 有什麼區別?」是 C# 面試經典題,但許多回答停留在「比較值 vs 比較參考」的過度簡化。本文從型別系統根源、靜態/虛擬方法、運算子多載、實際測試代碼四個維度,建立精確、完整的認知框架。
// 參考型別
public class ReferenceClass
{
public int X { get; set; }
public string Y { get; set; }
}
// 值型別
public struct ValueStruct
{
public int X;
public string Y;
}
var c1 = new ReferenceClass { X = 1, Y = "OK" };
var c2 = new ReferenceClass { X = 1, Y = "OK" }; // 內容相同,不同實例
var c3 = new ReferenceClass { X = 1, Y = "OK!" }; // 內容不同
var c4 = new ReferenceClass { X = 2, Y = "OK" }; // 內容不同
var c5 = c1; // 同一參考
var c6 = c1; // 同一參考
c5.X = 2; // 修改 c5 影響 c1
// 測試四種比較方式
Console.WriteLine("=== ReferenceEquals (靜態) ===");
Console.WriteLine(ReferenceEquals(c1, c2)); // False
Console.WriteLine(ReferenceEquals(c1, c3)); // False
Console.WriteLine(ReferenceEquals(c1, c4)); // False
Console.WriteLine(ReferenceEquals(c1, c5)); // True
Console.WriteLine(ReferenceEquals(c1, c6)); // True
Console.WriteLine("=== Equals 靜態方法 ===");
Console.WriteLine(Equals(c1, c2)); // False
Console.WriteLine(Equals(c1, c3)); // False
Console.WriteLine(Equals(c1, c4)); // False
Console.WriteLine(Equals(c1, c5)); // True
Console.WriteLine(Equals(c1, c6)); // True
Console.WriteLine("=== 實例虛擬方法 c1.Equals() ===");
Console.WriteLine(c1.Equals(c2)); // False
Console.WriteLine(c1.Equals(c3)); // False
Console.WriteLine(c1.Equals(c4)); // False
Console.WriteLine(c1.Equals(c5)); // True
Console.WriteLine(c1.Equals(c6)); // True
Console.WriteLine("=== == 運算子 ===");
Console.WriteLine(c1 == c2); // False
Console.WriteLine(c1 == c3); // False
Console.WriteLine(c1 == c4); // False
Console.WriteLine(c1 == c5); // True
Console.WriteLine(c1 == c6); // True
四種方式全部比較參考位址 (預設行為)。ReferenceEquals、Equals 靜態、Equals 實例、== 結果完全一致。
var v1 = new ValueStruct { X = 1, Y = "A" };
var v2 = new ValueStruct { X = 1, Y = "A" }; // 內容相同
var v3 = new ValueStruct { X = 2, Y = "A" };
var v4 = v1; // 複本 (值型別賦值即複製)
Console.WriteLine("=== ReferenceEquals (值型別) ===");
Console.WriteLine(ReferenceEquals(v1, v2)); // False (裝箱產生不同物件)
Console.WriteLine(ReferenceEquals(v1, v1)); // False (每次裝箱新物件)
Console.WriteLine("=== Equals 靜態/實例 ===");
Console.WriteLine(Equals(v1, v2)); // True (內容相等)
Console.WriteLine(v1.Equals(v2)); // True
Console.WriteLine("=== == 運算子 ===");
// v1 == v2 編譯錯誤:值型別預設不支援 ==,除非自行多載
false (除非 null),因裝箱產生不同堆上物件| 機制 | 分類 | 參考型別預設行為 | 值型別預設行為 | 可自訂 | Null 安全 |
|---|---|---|---|---|---|
ReferenceEquals | 靜態方法 | 參考位址比較 | 永遠 false (裝箱) | ❌ | ✅ (null==null 為 true) |
Equals(object) 靜態 | 靜態方法 | 參考位址比較 | 內容值比較 | 間接 (透過實例虛擬方法) | ✅ (內部檢查 null) |
Equals(object) 實例虛擬 | 虛擬方法 | 參考位址比較 | 內容值比較 (ValueType 覆寫) | ✅ (覆寫) | ❌ (this 為 null 拋例外) |
== 運算子 | 靜態運算子 | 參考位址比較 | 不可用 (需多載) | ✅ (多載) | 視實作而定 |
null 檢查
public static bool Equals(object objA, object objB)
{
if (objA == objB) return true; // 同參考或同為 null
if (objA == null || objB == null) return false;
return objA.Equals(objB); // 委託給實例方法
}
null,不拋例外EqualsObject.Equals:參考比較ValueType.Equals:反射逐欄位比較 (效能差,建議覆寫)String.Equals:內容比較 (已優化)GetHashCodeReferenceEquals)public static bool operator ==(T a, T b)Equals(object)、GetHashCode()、多載 ==/!=、實作 IEquatable<T>Equals 避免反射效能損失,多載 ==/!=ReferenceEquals 或 object.ReferenceEqualsEquals(objA, objB) 靜態方法== 和 Equals 皆為內容比較,但 Equals 可指定 StringComparison (序數/文化特性/忽略大小寫)精準掌握三者差異,不僅能通過面試,更能避免生產環境中因比較邏輯錯誤導致的詭異 Bug。