所有值型別繼承 System.ValueType,預設獲得 Equals 與 GetHashCode 實作。但預設實作存在效能陷阱與正確性風險,微軟強烈建議覆寫。本文基於 SSCLI 2.0 (Rotor) 原始碼,追蹤 ValueType.Equals 完整呼叫鏈、快速比較路徑判斷邏輯、MethodTable 旗標結構,揭示 CLR 如何在執行期決定比較策略。
public override bool Equals(object obj)
{
if (obj == null) return false;
RuntimeType thisType = (RuntimeType)this.GetType();
RuntimeType thatType = (RuntimeType)obj.GetType();
if (thatType != thisType) return false; // 型別不符直接 false
object thisObj = (object)this; // 裝箱
object thisResult, thatResult;
// 關鍵分支:是否可用快速位元比較
if (CanCompareBits(this))
return FastEqualsCheck(thisObj, obj); // memcmp 記憶體區塊比較
// 回退:反射逐欄位比較
FieldInfo[] fields = thisType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
for (int i = 0; i < fields.Length; i++)
{
thisResult = ((RtFieldInfo)fields[i]).InternalGetValue(thisObj, false);
thatResult = ((RtFieldInfo)fields[i]).InternalGetValue(obj, false);
if (thisResult == null)
{
if (thatResult != null) return false;
}
else if (!thisResult.Equals(thatResult)) // 遞迴呼叫 Equals
return false;
}
return true;
}
// Managed 端進入點
FCIMPL1(FC_BOOL_RET, ValueTypeHelper::CanCompareBits, Object* obj)
{
WRAPPER_CONTRACT;
STATIC_CONTRACT_SO_TOLERANT;
_ASSERTE(obj != NULL);
MethodTable* mt = obj->GetMethodTable();
// 核心判斷:無指標欄位 且 緊密封裝
FC_RETURN_BOOL(!mt->ContainsPointers() && !mt->IsNotTightlyPacked());
}
FCIMPLEND
FCIMPL2(FC_BOOL_RET, ValueTypeHelper::FastEqualsCheck, Object* obj1, Object* obj2)
{
WRAPPER_CONTRACT;
STATIC_CONTRACT_SO_TOLERANT;
_ASSERTE(obj1 != NULL);
_ASSERTE(obj2 != NULL);
_ASSERTE(!obj1->GetMethodTable()->ContainsPointers());
_ASSERTE(obj1->GetSize() == obj2->GetSize());
TypeHandle pTh = obj1->GetTypeHandle();
// 直接記憶體區塊比較
FC_RETURN_BOOL(memcmp(obj1->GetData(), obj2->GetData(), pTh.GetSize()) == 0);
}
FCIMPLEND
// MethodTable 旗標定義 (class.h)
enum FlagBits : DWORD
{
// ... 其他旗標 ...
enum_flag_ContainsPointers = 0x00080000, // 含有物件參考欄位 (GC 需掃描)
enum_flag_NotTightlyPacked = 0x04000000, // 記憶體佈局非緊密 (有填充/對齊間隙)
};
// 執行期檢查
DWORD ContainsPointers() { return m_wFlags & enum_flag_ContainsPointers; }
BOOL IsNotTightlyPacked() { return m_wFlags & enum_flag_NotTightlyPacked; }
載入類別佈局時,掃描所有實例欄位,若存在 Object 參考型別欄位 → 設定旗標。
// Class.cpp: MethodTableBuilder::BuildMethodTableThrowing
if (IsValueClass() && (GetNumInstanceFieldBytes() != totalDeclaredFieldSize || HasOverlaidField()))
{
pMT->SetNotTightlyPacked();
}
// IsValueClass 判斷
inline DWORD IsValueClass()
{
return m_VMFlags & VMFLAG_VALUETYPE; // 0x00800000
}
// VMFLAG_VALUETYPE 設定邏輯
if (pParentMethodTable != NULL &&
((g_pEnumClass != NULL && pParentMethodTable == g_pValueTypeClass) || pParentMethodTable == g_pEnumClass))
{
SetValueClass(); // 父類別為 ValueType 或 Enum
}
| 條件 | 含義 | 典型案例 |
|---|---|---|
GetNumInstanceFieldBytes() != totalDeclaredFieldSize | 欄位實際大小總和 ≠ 宣告大小總和 | 結構體有填充位元組 (對齊)、含布林值 (1 byte 但對齊到 4/8 byte) |
HasOverlaidField() | 欄位重疊 (Explicit Layout) | [StructLayout(LayoutKind.Explicit)]、[FieldOffset]、聯集 |
// class.h: Enum TrustLevel
enum TrustLevel
{
KNone = 0, // 無保證,拒絕載入
KLegal = 1, // 保證無 objref<->scalar 重疊,載入但欄位存取不驗證
KVerifiable = 2, // 保證無 objref<->objref 重疊 + 以上,載入且驗證通過
KNonOverlaid = 3 // 保證完全無重疊 + 以上,**Equals 可優化若緊密封裝**
};
關鍵發現:KNonOverlaid (3) 等級明確註釋:Equals() may be optimized if structure is tightly packed。CLR 載入器根據結構體定義計算 TrustLevel,達到 KNonOverlaid 且緊密封裝時,才允許 FastEqualsCheck 優化。
struct Test { public float F; }
var a = new Test { F = -0.0f };
var b = new Test { F = +0.0f };
// 邏輯相等,但 memcmp 比較位元模式不同 → FastEqualsCheck 回傳 false
// JIT 優化後行為可能改變,導致非確定性結果
這是 CLR 設計權衡:極致效能 (memcmp) vs 語意正確性 (浮點數特殊值)。開發者覆寫 Equals 可避免此風險。
public struct Point : IEquatable<Point>
{
public int X, Y;
public override bool Equals(object obj) => obj is Point p && Equals(p);
public bool Equals(Point other) => X == other.X && Y == other.Y;
public override int GetHashCode() => HashCode.Combine(X, Y); // .NET Core 2.1+
public static bool operator ==(Point a, Point b) => a.Equals(b);
public static bool operator !=(Point a, Point b) => !a.Equals(b);
}
深入 CLR 內部機制,不僅能理解預設行為的限制,更能設計高效能、正確的值型別,這是系統級 .NET 開發者的核心童子功。