BeforeFieldInit 與類靜態建構函式
羅朝輝 (http://kesalin.cnblogs.com/)
本文遵循“署名-非商業用途-保持一致”創作公用協議
如下代碼:
using System;namespace BeforeFieldInit{ internal class Foo { Foo(){ Console.WriteLine("Foo 物件建構函數");} public static string Field = GetString("初始化 Foo 靜態成員變數!"); public static string GetString(string s){ Console.WriteLine(s); return s; } } internal class FooStatic { static FooStatic(){ Console.WriteLine("FooStatic 類建構函式"); } FooStatic(){ Console.WriteLine("FooStatic 物件建構函數"); } public static string Field = GetString("初始化 FooStatic 靜態成員變數!"); public static string GetString(string s){ Console.WriteLine(s); return s; } } class Program { static void Main(string[] args){ Console.WriteLine("Main 開始 ..."); Foo.GetString("手動調用 Foo.GetString() 方法!"); //string info = Foo.Field; FooStatic.GetString("手動調用 FooStatic.GetString() 方法!"); //string infoStatic = FooStatic.Field; Console.ReadLine(); } }}
Foo 和 FooStatic 唯一的不同就是 FooStatic 有靜態類建構函式。執行上面的代碼,輸出如下:
如果把被注釋的讀取靜態欄位Field的兩行代碼開啟,再編譯運行,輸出:
對比上面的區別,FooStatic 始終是延遲裝載的,也就是只有類被首次使用時,類對象才被構造,其靜態成員以及靜態建構函式才被初始化執行, 而 Foo 類對象的初始化則交給 CLR 來決定。
如果用 IL Dasm.exe對比兩個類產生的中間代碼,可以看到只有一處不同:FooStatic 比 Foo 少了一個特性:beforefieldinit。
也就是說靜態建構函式抑制了 beforefieldinit 特性,而該特性會影響對調用該類的時機。
C# 裡面的靜態建構函式,也稱為類型構造器,類型初始化器,它是私人的,就是在中的 .cctor : void()。CLR保證一個靜態建構函式在每個AppDomain中只執行一次,而且這種執行是安全執行緒的,所以在靜態建構函式中非常適合於單例模式的初始化(初始化靜態欄位等同於在靜態建構函式中初始化,但不完全相同,因為顯式定義靜態建構函式會抑制beforefieldinit標誌。)。
JIT編譯器在編譯一個方法時,會查看代碼中引用了哪些類型,任何一個類型定義了靜態建構函式,JIT編譯器都會檢查針對當前 AppDomain,是否執行了這個靜態建構函式。如果類型構造去沒有執行,JIT編譯器就會在產生的本地代碼中添加對靜態建構函式的一個調用,否則就不會添加,因為類型已經初始化。同時CLR還保證在執行本地代碼中產生的靜態構造函代碼的安全執行緒。
根據上面的描述,我們知道 JIT 必須決定是否組建類型靜態建構函式代碼,還須決定何時調用它。具體在何時調用有兩中方式:
precise:JIT編譯器可以剛好在建立類型的第一個執行個體之前,或剛好在訪問類的一個非繼承的欄位或成員之前生產這個調用。
beforefieldinit:JIT編譯器可以在首次訪問一個靜態欄位或者一個靜態/執行個體方法之前,或者建立類型的第一個執行個體之前,隨便找一個時間產生調用。具體調用時機由CLR決定,它只保證訪問成員之前會執行靜態建構函式,但可能會提前很早就執行。
CLI specification (ECMA 335) 在 8.9.5 節中提到:
- If marked BeforeFieldInit then the type's initializer method is executed at, or sometime before, first access to any static field defined for that type
- If not marked BeforeFieldInit then that type's initializer method is executed at (i.e., is triggered by):
- first access to any static or instance field of that type, or
- first invocation of any static, instance or virtual method of that type
簡單點說就是beforefieldinit可能會提前調用一個類型的靜態建構函式,而precise模式是非要等到用時才調用類型的靜態建構函式,它是嚴格的延遲裝載。
beforefieldinit 是首選的(如果沒有自訂靜態建構函式,預設就是這種方式),因為它使CLR能夠自由選擇調用靜態建構函式的時機,而CLR會儘可能利用這一點來產生運行得更快的代碼。比如說在一個迴圈中調用單例(且包含首次調用),beforefieldinit方式可以讓CLR決定在迴圈之前就調用靜態建構函式來最佳化,而precise模式則只會在迴圈體中來調用靜態建構函式,並在之後的調用會檢測靜態建構函式是否已被執行的標誌位,這樣效率稍低一些。在前面使用靜態Field的情況下,beforefieldinit 方式下CLR也認為提前執行靜態建構函式是更好的選擇。
C# 的單例實現,可以利用 precise 延遲調用這一點來延遲對單例對象的構造(餓汗模式),從而帶來一丁點的最佳化,但是在絕大部分情況下這一丁點的最佳化作用並不大!