[C#]BeforeFieldInit與類靜態構造函數

      最近看到一篇關於 C# 語言中關於靜態構造函數延遲加載核心原理的文章,感受很好特轉一下,以供朋友們共同進步! (我的不多轉載他人文章)
安全


以下代碼:
 
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 有靜態的類構造函數。執行上面的代碼,輸出以下:ide

wKioL1j9bJmwWYkZAAAS97GYgKA327.png


  若是把被註釋的讀取靜態字段Field的兩行代碼打開,再編譯運行,輸出:函數

wKioL1j9bKWQ-6HdAAAQQaV0zHQ888.png



對比上面的區別,FooStatic 始終是延遲裝載的,也就是隻有類被首次使用時,類對象才被構造,其靜態成員以及靜態構造函數才被初始化執行,而Foo 類對象的初始化則交給CLR 來決定。
若是用IL Dasm.exe對比兩個類生成的中間代碼,能夠看到只有一處不一樣:FooStatic 比Foo 少了一個特性:優化

wKioL1jYeIbwEJmDAABdz1IunUs999.png


也就是說靜態構造函數抑制了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 節中提到:
1. 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
2. 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 延遲調用這一點來延遲對單例對象的構造(餓汗模式),從而帶來一丁點的優化,可是在絕大部分狀況下這一丁點的優化做用並不大!spa

相關文章
相關標籤/搜索