在 Artech 大大的文章《關於 Type Initializer 和 BeforeFieldInit 的問題,看看大家能否給出正確的解釋》中,我們看到了一個關於型別建構器呼叫時機的有趣範例,並引發了對 beforefieldinit 影響型別建構器執行時機的探討。這為理解型別建構器提供了絕佳的實戰體驗。
本文作為補充,將從基礎出發層層深入,針對原文未詳細闡述的概念與原理進行必要補充——例如更全面地認識型別建構器、認識 BeforeFieldInit。在此基礎上,探討型別建構器的實踐應用,並期望能解答原文範例的執行結果。
在 .NET 中,類別的初始化過程在建構器中進行。依據建構成員的類型,分為型別建構器(.cctor)與物件建構器(.ctor),兩者分別為 IL 程式碼中的指令表示。.cctor 無法被直接呼叫,其呼叫規則正是本文重點;而 .ctor 會在型別實例化時自動被呼叫。
為了探討型別建構器,我們先實作一個包含一般建構器與靜態建構器的簡單類別定義:
// Release : code01, 2008/11/02
// Author : Anytao, https://www.anytao.com
public class User
{
static User()
{
message = "Initialize in static constructor.";
}
public User()
{
message = "Initialize in normal constructor.";
}
public User(string name, int age)
{
Name = name;
Age = age;
}
public string Name { get; set; }
public int Age { get; set; }
public static string message = "Initialize when defined.";
}

使用 ILDasm.exe 反編譯可清晰看到對應的型別建構器與物件建構器。
在產生的 IL 程式碼中可看到對應的 ctor,型別實例化時會執行對應建構器進行初始化。關於實例化的複雜執行順序,可參閱《你必須知道的.NET》第 7.8 節「動靜之間:靜態和非靜態」,本文不再贅述。
用於執行靜態成員的初始化。在 .NET 中,型別在兩種情況下會發生 .cctor 的呼叫:
.cctor 的 IL 實作為單純的靜態欄位賦值。.cctor 的 IL 程式碼會依序包含這兩部分操作。靜態建構函數的基本規則包括:
因為型別建構器的特性,.cctor 的呼叫時機並非確定概念。由於型別建構器皆為 private,使用者無法顯式呼叫。.NET 中主要有兩種方案:
兩者執行差異主要在於是否實作了顯式的靜態建構函數:若實作了顯式靜態建構函數,則採 precise 方式;若未實作,則採 beforefieldinit 方式。
當類別顯式實作靜態建構函數時:
public class User
{
static User()
{
message = "Initialize in static constructor.";
}
public static string message = "Initialize when defined.";
}
對應 IL 中類別宣告不含 beforefieldinit 標記。CLR 必須在第一次存取該型別的靜態成員或建立實例之前,精確執行型別建構器。
當類別未實作顯式靜態建構函數,僅有靜態欄位初始化時:
public class User
{
public static string message = "Initialize when defined.";
}
對應 IL 中類別宣告包含 beforefieldinit 標記:
.class public auto ansi beforefieldinit User
從 IL 實作來看,兩者內部邏輯基本相同,差異在於中繼資料宣告上是否標記 beforefieldinit。Scott Allen 解釋:beforefieldinit 讓 CLR 擁有在任何時候執行 .cctor 的授權,只要在第一次存取型別靜態欄位前執行即可。
.cctor,語意精確。.cctor,一定程度優化效能,較 precise 方式更高效。值得注意的是,當多個 beforefieldinit 型別建構器共存時,CLR 無法保證它們的執行順序,實務編碼應盡量避免此情況。
針對 Artech 原文中的範例分析:
Main 中是否宣告 string field = Foo.Field;,結果不受影響。C# 規格中對 beforefieldinit 的控制引起關注:一方面可優化效能,但以顯式/隱式實作靜態建構函數來控制不夠直觀,期待未來版本能以屬性宣告方式控制。另一方面,兩個型別建構器相互引用時,CLR 無法保證呼叫順序,開發者應避免在型別建構器中編寫依賴順序的業務邏輯。
本文補充了 Artech 的問題,延續了《你必須知道的.NET》第 7.8 節對型別建構器的探討,以更全面視角闡釋此議題。雖未 100% 掌握 Debug 模式下 CLR 呼叫行為,但深入細節讓我們對語言內部有更多掌控與理解,這正是開始。