类型构造器(静态构造函数)的执行时机你知道多少?

一、概念

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控制的,程序员不能准确把握运行时机。

原文地址:https://www.cnblogs.com/skm-blog/p/4224113.html