類型構造器(靜態構造函數)的執行時機你知道多少?


一、概念

1、類型構造器也稱為靜態構造器(static constructor)或者類型初始化器(type initializer),和實例構造器類似,類型構造器是設置類型的初始化狀態。

2、類型構造器如果定義,只能定義一個且不能有任何參數,不能有任何訪問修飾符(會默認為private),因為它是由CLR自行調用的,不能由程序員手動調用,整個AppDomain中只執行一次(線程安全的)。

3、由於CLR保證一個類型構造器在一個AppDomain中只執行一次,而且這個執行是線程安全的,所以非常合適在類型構造器中初始化類型需要的任何單例(Singleton)對象,詳情請參考  設計模式之單例模式 

4、類型構造器究竟什么時候調用?不同的調用時機又會產生怎樣的效果呢?有什么性能問題呢?這是我們討論的重點。Artech的  關於Type Initializer和 BeforeFieldInit的問題,看看大家能否給出正確的解釋  

     這篇文章中提到的問題可以很好的測試你是否對類型構造器有足夠的理解。

二、調用時機

申明:本模塊的測試Demo是參考Artech的 關於Type Initializer和 BeforeFieldInit的問題,看看大家能否給出正確的解釋 這篇文章寫的,我在這里給出答案。

1、實驗一

namespace DoNet.Seven.ConsoleApplicationTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Start ...");
            beforFieldInit.Method("Manually invoke the static Method() method!");
            Console.ReadKey();
        }
    }
    class beforFieldInit
    {
        //靜態字段
        public static string Field = Method("Initialize the static field!");
        //靜態方法
        public static string Method(string s)
        {
            Console.WriteLine(s);
            return s;
        }
    }
}

注釋:在beforFieldInit中我們以內聯的方式初始化字段Field,由於CLR保證了在調用類的實例或者靜態成員之前 需要先調用類型構造器(靜態構造函數),所以這個結果很好理解。

2、實驗二

namespace DoNet.Seven.ConsoleApplicationTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Start ...");
            beforFieldInit.Method("Manually invoke the static Method() method!");
            string filed = beforFieldInit.Field;
            Console.ReadKey();
        }
    }
    class beforFieldInit
    {
        //靜態字段
        public static string Field = Method("Initialize the static field!");
        //靜態方法
        public static string Method(string s)
        {
            Console.WriteLine(s);
            return s;
        }
       
    }
}

注釋:當我們在Main中添加代碼 string filed = beforFieldInit.Field;結果就不一樣了,這是因為先執行了類型構造器(靜態構造函數)的原因,那為什么會先執行類型構造器呢?

這其實是編譯器的一種優化方案,當編譯器發現我們需要調用靜態字段的時候,因為CLR保證了在靜態字段使用之前類型構造器是一定執行結束了。所以編譯器就會先執行靜態構造器(這就是beforfieldinit的一種優化策略)。

3、實驗三

namespace DoNet.Seven.ConsoleApplicationTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Start ...");
            beforFieldInit.Method("Manually invoke the static Method() method!");
            string filed = beforFieldInit.Field;
            Console.ReadKey();
        }
    }
    class beforFieldInit
    {
        //靜態字段
        public static string Field = Method("Initialize the static field!");
        //靜態方法
        public static string Method(string s)
        {
            Console.WriteLine(s);
            return s;
        }
        static beforFieldInit() { }
    }
}

注釋:當我們在beforFieldInit類中手動添加靜態構造函數的時候,執行順序又正常了,那這又是為什么呢?

         其實當我們手動添加靜態構造函數的時候,靜態構造函數的執行時機將不再采用beforfieldinit方式,而是采用precise方式,precise方式則是要求恰好在第一次創建類型的實例或者使用靜態成員之前執行,就是用之前剛剛執行。

4、beforefieldinit方式和precise方式的區別

precise:JIT編譯器剛好在創建類型的第一個實例之前或者剛好在訪問類的一個非繼承的字段或者成員之前生成這個調用。這稱為“精確”語義,

beforefieldinit:JIT編譯器可以在首次訪問一個靜態字段或者一個靜態或者實例方法之前,或者在調用一個實例構造器之前,隨便找一個時間生成調用。這稱為“字段初始化前”語義。

5、結論

C#編譯器看到一個類包含進行了內聯初始化的靜態字段,會在類的類型定義中生成一個添加了BeforeFieldInit元數據標記的記錄項。C#編譯器如果看到一個類顯示實現了類型構造器,就不會添加BeforeInit元數據標記。

三、類型構造器的性能

namespace DoNet.Seven.ConsoleApplicationTest
{
    class Program
    {
        static void Main(string[] args)
        {
            const int N = 2000 * 1000 * 1000;
            test1(N);
            test2(N);
            Console.ReadKey();


        }
        //調用test1方法時,BeforeFieldInit和Precise的類型構造器都沒有執行,將嵌入到test1中,使test1變慢。
        static void test1(int n)
        {
            Stopwatch sw = Stopwatch.StartNew();
            for(int i=0;i<=n;i++)
            {
                //BeforeFieldInit類中沒有顯示申明靜態構造函數,所以靜態構造函數在test1之前就已經執行了,不影響性能
                BeforeFieldInit.x = 10;
            }
            Console.WriteLine(string.Format("BeforeFieldInit方式調用靜態構造函數運行時長:{0}", sw.Elapsed));
            sw = Stopwatch.StartNew();
            for(int i=0;i<=n;i++)
            {
                //Precise類中顯示聲明了靜態構造函數,所以靜態構造函數在test1中進行檢查,每次都會檢查是是否調用
                Precise.x = 10;
            }
            Console.WriteLine(string.Format("Precise方式調用靜態構造函數運行時長:{0}", sw.Elapsed));
        }
        //因為test1中已經調用了靜態構造函數,所以test2不會生產對靜態構造函數的調用
        static void test2(int n)
        {
            Stopwatch sw = Stopwatch.StartNew();
            for (int i = 0; i <= n; i++)
            {
                BeforeFieldInit.x = 10;
            }
            Console.WriteLine(string.Format("BeforeFieldInit方式調用靜態構造函數運行時長:{0}", sw.Elapsed));
            sw = Stopwatch.StartNew();
            for (int i = 0; i <= n; i++)
            {
                Precise.x = 10;
            }
            Console.WriteLine(string.Format("Precise方式調用靜態構造函數運行時長:{0}", sw.Elapsed));
        }
    }
    class BeforeFieldInit
    {
        public static int x = 1;
    }
    class Precise
    {
        public static int x;
        static Precise(){x=1;}
    }
}

從輸入我們可以看出,靜態構造函數如果使用不當,對性能會產生很大影響的,test1中的差別很明顯,一個4秒,一個6秒。

四、建議

1、不要在靜態構造函數中執行復雜的邏輯、它只是為了對靜態字段進行初始化而設置的

2、不要出現兩個或者多個類的靜態構造函數相互調用的情況,因為它是線程安全的,是要加鎖的,如果出現相互調用,可能導致死鎖。

3、不要在類的靜態構造函數中寫你期望按照某個順序執行的代碼邏輯,因為靜態構造函數的調用時由CLR控制的,程序員不能准確把握運行時機。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM