反射不僅是「運行時檢查類型」,更涉及**類型初始化的精確時序**。一道經典面試題揭示:靜態字段內聯初始化 → 靜態構造函數 → 基類靜態構造函數 → 基類實例字段初始化 → 基類實例構造函數 → 派生類實例字段初始化 → 派生類實例構造函數。本文通過反編譯 IL、實測驗證,徹底講透這一機制。
public class MyTest {
public MyTest(string info) => Console.WriteLine(info);
}
public class BaseA {
public static MyTest a1 = new MyTest("a1"); // 靜態字段內聯
public MyTest a2 = new MyTest("a2"); // 實例字段內聯
static BaseA() { new MyTest("a3"); } // 靜態構造函數
public BaseA() { new MyTest("a4"); } // 實例構造函數
public virtual void MyFun() { new MyTest("a5"); } // 虛方法
}
public class BaseB : BaseA {
public static MyTest b1 = new MyTest("b1");
public MyTest b2 = new MyTest("b2");
static BaseB() { new MyTest("b3"); }
public BaseB() { new MyTest("b4"); }
public new void MyFun() { new MyTest("b5"); } // new 隱藏
}
static void Main() {
var b = new BaseB();
b.MyFun();
}
b1 // 1. 派生類靜態字段內聯
b3 // 2. 派生類靜態構造函數
b2 // 3. 派生類實例字段內聯
a1 // 4. 基類靜態字段內聯
a3 // 5. 基類靜態構造函數
a2 // 6. 基類實例字段內聯
a4 // 7. 基類實例構造函數
b4 // 8. 派生類實例構造函數
b5 // 9. 派生類 new 方法調用 (非虛調用)
註:a5 **未輸出**,因 BaseB.MyFun 用 new 隱藏而非 override,調用 b.MyFun() 靜態綁定到 BaseB.MyFun。
// C# 源碼
public int X = 5;
// IL 等效 (簡化)
public int X;
public BaseA() { X = 5; /* 然後調用基類構造函數 */ }
BaseB 實例需先確保 BaseB 類型初始化完成,CLR 遞歸觸發基類 .cctor?不,**實測派生類 .cctor 先執行**!原因:CLR 按「最先被引用的類型」初始化。Main 中首次引用 BaseB (new BaseB()),故先跑 BaseB.cctor,其內部隱式引用基類靜態成員才觸發 BaseA.cctor。故順序:a2 (基類字段) → a4 (基類構造) → b4 (派生構造)。但注意 b2 (派生類字段內聯) 在 BaseB.cctor 後、**實例構造鏈前** 執行?**實測順序是 b2 在 b3 後、a1 前**。
修正完整順序邏輯:
BaseB 類型初始化BaseB.cctor 執行:派生類靜態字段內聯 (b1) → 靜態構造函數體 (b3)BaseB.cctor 結束,**隨後立即執行派生類實例字段內聯 (b2)**?不!實例字段初始化屬於 .ctor 階段。BaseB 的實例字段內聯初始化代碼被編譯器放在 BaseB.ctor 中,**但** BaseB.ctor **尚未調用**!為什麼 b2 會在 a1 前輸出?BaseB 繼承 BaseA,但 BaseB **未顯式調用基類構造函數** (無 : base()),編譯器生成隱式 base() 調用。**然而**,C# 編譯器為派生類生成的 .ctor 中,**實例字段內聯初始化代碼位於 base() 調用之前**!a1 (基類靜態字段) 何時執行?在 BaseA.cctor 中。BaseA.cctor 由 BaseB.cctor 隱式觸發 (如訪問基類靜態成員) 或 CLR 在實例化前自動觸發。**實測 b2 在 a1 前**,說明 BaseA.cctor 觸發點在 BaseB.ctor 的 base() **之前**?結論:CLR 類型初始化順序極其微妙,**實測為準,不死記規則**。核心原則:
new 破壞虛方法調用鏈。Lazy<T> 或顯式初始化方法。Lazy<T> 或雙重檢查鎖,而非靜態構造函數。Call Stack 中的 [Native to Managed Transition] 確定觸發源。「靜態構造函數執行順序」是 C# 運行時最隱晦的角落之一。不依賴它編寫業務邏輯,是成熟工程師的標誌。