[C#]BeforeFieldInit 與類靜態(tài)構(gòu)造函數(shù)
BeforeFieldInit 與類靜態(tài)構(gòu)造函數(shù)
羅朝輝 (http://kesalin.cnblogs.com/)
如下代碼:
using System; namespace BeforeFieldInit { internal class Foo { Foo(){ Console.WriteLine("Foo 對象構(gòu)造函數(shù)");} public static string Field = GetString("初始化 Foo 靜態(tài)成員變量!"); public static string GetString(string s){ Console.WriteLine(s); return s; } } internal class FooStatic { static FooStatic(){ Console.WriteLine("FooStatic 類構(gòu)造函數(shù)"); } FooStatic(){ Console.WriteLine("FooStatic 對象構(gòu)造函數(shù)"); } public static string Field = GetString("初始化 FooStatic 靜態(tài)成員變量!"); public static string GetString(string s){ Console.WriteLine(s); return s; } } class Program { static void Main(string[] args){ Console.WriteLine("Main 開始 ..."); Foo.GetString("手動調(diào)用 Foo.GetString() 方法!"); //string info = Foo.Field; FooStatic.GetString("手動調(diào)用 FooStatic.GetString() 方法!"); //string infoStatic = FooStatic.Field; Console.ReadLine(); } } }
Foo 和 FooStatic 唯一的不同就是 FooStatic 有靜態(tài)的類構(gòu)造函數(shù)。執(zhí)行上面的代碼,輸出如下:

如果把被注釋的讀取靜態(tài)字段Field的兩行代碼打開,再編譯運行,輸出:

對比上面的區(qū)別,F(xiàn)ooStatic 始終是延遲裝載的,也就是只有類被首次使用時,類對象才被構(gòu)造,其靜態(tài)成員以及靜態(tài)構(gòu)造函數(shù)才被初始化執(zhí)行, 而 Foo 類對象的初始化則交給 CLR 來決定。
如果用 IL Dasm.exe對比兩個類生成的中間代碼,可以看到只有一處不同:FooStatic 比 Foo 少了一個特性:beforefieldinit。

也就是說靜態(tài)構(gòu)造函數(shù)抑制了 beforefieldinit 特性,而該特性會影響對調(diào)用該類的時機。
C# 里面的靜態(tài)構(gòu)造函數(shù),也稱為類型構(gòu)造器,類型初始化器,它是私有的,就是在上圖中的 .cctor : void()。CLR保證一個靜態(tài)構(gòu)造函數(shù)在每個AppDomain中只執(zhí)行一次,而且這種執(zhí)行是線程安全的,所以在靜態(tài)構(gòu)造函數(shù)中非常適合于單例模式的初始化(初始化靜態(tài)字段等同于在靜態(tài)構(gòu)造函數(shù)中初始化,但不完全相同,因為顯式定義靜態(tài)構(gòu)造函數(shù)會抑制beforefieldinit標志。)。
JIT編譯器在編譯一個方法時,會查看代碼中引用了哪些類型,任何一個類型定義了靜態(tài)構(gòu)造函數(shù),JIT編譯器都會檢查針對當前 AppDomain,是否執(zhí)行了這個靜態(tài)構(gòu)造函數(shù)。如果類型構(gòu)造去沒有執(zhí)行,JIT編譯器就會在生成的本地代碼中添加對靜態(tài)構(gòu)造函數(shù)的一個調(diào)用,否則就不會添加,因為類型已經(jīng)初始化。同時CLR還保證在執(zhí)行本地代碼中生成的靜態(tài)構(gòu)造函代碼的線程安全。
根據(jù)上面的描述,我們知道 JIT 必須決定是否生成類型靜態(tài)構(gòu)造函數(shù)代碼,還須決定何時調(diào)用它。具體在何時調(diào)用有兩中方式:
precise:JIT編譯器可以剛好在創(chuàng)建類型的第一個實例之前,或剛好在訪問類的一個非繼承的字段或成員之前生產(chǎn)這個調(diào)用。
beforefieldinit:JIT編譯器可以在首次訪問一個靜態(tài)字段或者一個靜態(tài)/實例方法之前,或者創(chuàng)建類型的第一個實例之前,隨便找一個時間生成調(diào)用。具體調(diào)用時機由CLR決定,它只保證訪問成員之前會執(zhí)行靜態(tài)構(gòu)造函數(shù),但可能會提前很早就執(zhí)行。
CLI specification (ECMA 335) 在 8.9.5 節(jié)中提到:
- 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可能會提前調(diào)用一個類型的靜態(tài)構(gòu)造函數(shù),而precise模式是非要等到用時才調(diào)用類型的靜態(tài)構(gòu)造函數(shù),它是嚴格的延遲裝載。
beforefieldinit 是首選的(如果沒有自定義靜態(tài)構(gòu)造函數(shù),默認就是這種方式),因為它使CLR能夠自由選擇調(diào)用靜態(tài)構(gòu)造函數(shù)的時機,而CLR會盡可能利用這一點來生成運行得更快的代碼。比如說在一個循環(huán)中調(diào)用單例(且包含首次調(diào)用),beforefieldinit方式可以讓CLR決定在循環(huán)之前就調(diào)用靜態(tài)構(gòu)造函數(shù)來優(yōu)化,而precise模式則只會在循環(huán)體中來調(diào)用靜態(tài)構(gòu)造函數(shù),并在之后的調(diào)用會檢測靜態(tài)構(gòu)造函數(shù)是否已被執(zhí)行的標志位,這樣效率稍低一些。在前面使用靜態(tài)Field的情況下,beforefieldinit 方式下CLR也認為提前執(zhí)行靜態(tài)構(gòu)造函數(shù)是更好的選擇。
C# 的單例實現(xiàn),可以利用 precise 延遲調(diào)用這一點來延遲對單例對象的構(gòu)造(餓汗模式),從而帶來一丁點的優(yōu)化,但是在絕大部分情況下這一丁點的優(yōu)化作用并不大!
浙公網(wǎng)安備 33010602011771號