類型構造器也稱為靜態構造器,類構造器,或類型初始化器
類型構造器可以用於接口(C#不允許這樣做),引用類型,值類型。實例構造器用來設置一個類型某個實例的初始化狀態,類型構造器用來設置一個類型的初始化狀態。默認情況下,類型沒有定義類型構造器。下面展示如何定義值類型和引用類型的構造器:
internal sealed class SomeRefType { static SomeRefType() { } } internal struct SomeValType { static SomeValType() { } }
可以發現一個特點是:無參,static標記,而且可訪問性都是private,但是不能顯示指定為private。當我們在值類型里面定義了一個類型構造器時,CLR不一定會調用這個靜態構造器,如:
internal struct SomeValType { static SomeValType() { Console.WriteLine("不會展示出來"); } public Int32 m_x; } public class Program { static void Main() { SomeValType[] a = new SomeValType[10]; a[0].m_x = 123; Console.WriteLine(a[0].m_x);//顯示123 } }
當JIT編譯器編譯一個方法時,它會檢查在代碼里面是否引入了其他類型。如果引入的的其他類型定義了類型構造器,則JIT會檢測是否已經在AppDomain里面執行過。如果沒有執行,則發起對類型構造器的調用,否則不調用。
在編譯之后,線程會開始執行並最終獲取調用構造器的代碼。實際上有可能會是多個線程執行同一個方法,CLR想要確保一個類型構造器在一個AppDomain里面只執行一次,當一個類型構造器被調用時,調用的線程會獲取一個互斥的線程同步鎖,這時如果有其他的線程在調用,則會阻塞。當第一個線程執行完后離開,其他的線程被喚醒並發現構造器的代碼執行過了,所以不會繼續去執行了,從構造器方法返回。CLR通過這種方式來確保構造器僅僅被執行一次。
提示:
由於CLR會確保類型構造器在每一個AppDomain里面只會執行一次,是線程安全的。所以如果要初始化任何單例對象(singleton object),放在類型構造器里面是再合適不過了。
有這樣一種情況,ClassA的類型構造器包含引用ClassB的代碼,ClassB的類型構造器包含引用ClassA的代碼,這種情況下CLR仍然會保證類型構造器只執行一次,但是應該盡量避免這種場景出現,因為這里應該避免調用類型構造器有了具體的順序。
在類型構造器里面的代碼僅僅訪問的是類型的靜態字段,通常的意圖是初始化這些字段。C#提供了簡單的語法來實現:
internal sealed class SomeType { private static Int32 s_x = 5; }
上面的代碼在生成時,編譯器自動會為SomeType創建一個類型構造器如下:
internal sealed class SomeType { private static Int32 s_x; static SomeType() { s_x = 5; } }
但是,C#不允許值類型使用內聯字段初始化語法來實例化字段,所以下面這種方式就是錯的:
internal sealed struct SomeType { private Int32 s_x = 5; //這樣會報錯,需要加static關鍵字 }
如果顯示的定義了類型構造器,如下:
internal sealed class SomeType { private static Int32 s_x = 5; static SomeType() { s_x = 10; } }
最終s_x的結果是10。這里,C#編譯器首先會生成一個類型構造器方法,這個構造器首先初始化s_x為5,然后初始化為10。換句話說,在類型構造器里面的顯示定義的代碼會在實例化靜態字段之后執行。
類型構造器性能
調用類型構造器並不那么簡單,JIT編譯器不得不決定是否生成調用它的代碼,並且CLR要確保調用是線程安全的。當編譯器決定發起一個調用來執行類型構造器,它必須判斷是否應該這樣做,有兩種可能性:
1.JIT在創建類型的第一個實例的代碼之前立即發起或者在訪問類的非繼承的字段,成員的代碼之前立即調用
2.JIT在首次訪問一個靜態字段,靜態方法,實例方法,或調用一個實例構造器的代碼之前某個時間調用,因為CLR要確保靜態構造器在其他成員被訪問之前運行。
注 《CLR via C#》(Jeffrey Richter著)——.NET 界的經典之作,讀的過程寫點筆記跟大家分享,我也推薦大家看英文版,能夠直接領會原意