C# 静态构造函数(类型构造器)的要点

在C#中 类型构造器(type constructor),也称为静态构造器(static constructor)、类构造器(class constructor)、类型初始化器(type initializer)。类型构造器可应用于接口(目前C#编译器不支持)、引用类型和值类型。实例构造器的作用是设置类型的实例初始状态。类型构造器的作用是设置类型的初始状态。类型默认没有定义构造器,如果定义只能定义一个。此外类型构造器永远没有参数,而且必须标记为static,总是私有的。C#编译器会自动把它们标记为private.如果在源代码只能怪显示将类型构造器标记为private或者其它访问修饰符。C#编译就会抛出error.之所以私有,是为了阻止任何由开发人员写的代码调用,对类型构造器的调用总是由CLR负责的。

类型构造器的调用比较麻烦,JIT编译器在编译一个方法时,会查看代码中都引用了那些类型。任何一个定义了类型构造器的类型,JIT编译器都会检查针对当前AppDomain,是否已经执行了这个类型的构造器。如果构造器从未执行,JIT编译器会在它的本地(native)代码中添加对类型构造器的一个调用。如果类型构造器已经执行,JIT编译器就不添加对它的调用,因为JIT编译器知道类型已经初始化了。

现在,当方法被JIT编译完毕后,线程开始执行它,最终会执行到调用哪个类型构造器的代码。事实上,多个线程可能同时执行相同的方法。CLR希望确保在每个AppDomian(应用程序域)中,一个类型构造器只能执行一次,为了保证这点,在调用类型构造器时,调用线程要获取一个互斥同步锁。这样一来,在某一时间就会只用一个线程执行类型构造器中的代码了,第一个线程执行类型构造器中的代码,完事了第一个线程释放锁,当第一个线程离开构造器,正在等待的线程将被唤醒,然后发现类型构造器的代码已经被执行过,它就不会在执行这些代码,直接从构造器方法返回。除此之外如果再次调用这样的一个方法(代码所引用的一个类型定义了类型构造器),CLR知道类型构造器已被执行过,从而确保构造器不被再次调用。

类型构造器中的代码只能访问类型的静态字段。并且它的常规用途就是初始化这些字段。
CLR保证一个类型构造器在每个AppDomain中只执行一次,而且这种执行是线程安全的,所以非常适合在类型构造器中初始化类型的任何单利对象(Singleton).

虽然值类型可以定义一个类型构造器,但最好不要这么做,因为CLR不一定去调用类型构造器,如下例子

namespace NowCoderProgrammingProject
{
    struct SomeValType
    {
        public int x;
        static SomeValType()
        {
            Console.WriteLine("Static SomeValType");//不会输出
        }
    }
    class TestSomeValType
    {
        public static void Main()
        {
            SomeValType[] svtArr = new SomeValType[2];
            svtArr[0].x = 1;
            Console.WriteLine(svtArr[0].x);//输出1
        }
    }
}

类型构造器的性能

 在编译一个方法时,JIT编译器要决定是否在方法中生成一个对类型构造器的调用。如果JIT编译器决定生成这个调用,它还必须决定将这个调用添加到什么位置。具体什么位置,有以下两种可能:
 


  1. JIT编译器可以刚好在创建类型的第一个实例前,或者刚好在访问类的一个非继承的字段或成员之前生成这个调用。这称为”精确”(precise)语义,因为CLR调用类型构造器的时机拿捏恰到好处。
  2. JIT编译器可能在首次访问一个静态字段或一个静态/实例方法之前,或者在调用一个实例构造器之前,随便找个时间生成。这称为”字段初始化前”(before-field-init)语义,因为CLR只保证访问成员之前会运行类型构造器,可能提前很早就允许了。  

     “字段初始化前”语义是首选的,因为它是CLR能够自由选择调用类型构造器的时机。例如CLR可根据类型是在AppDomain中加载,还是域中立即加载(独立于AppDomain)或者进行JIT编译,还是用NGen来生成本地代码,从而选择不同的时机来调用类型构造器。
     
      默认情况下,语言的编译器会选择对你定义的类型来说最恰当的一种语义,并在类型定义元数据表的行中设置beforefieldinit标识,从而告诉CLR这个选择。   现在重点关注下C#编译器具体如何让选择,以及这些选择会对性能产生什么样的影响,如下代码:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NowCoderProgrammingProject
{
    public class UserBeforeFieldInit
    {
        public static string _name;
    }
    public class UserPrecise
    {
        public static string _name;
        static UserPrecise()
        {
            _name = "oec2003";
        }
    }
    class TestBeforeFieldInit
    {
        static void Main(string[] args)
        {
            const Int32 iterations = 1000 * 1000 * 1000;
            Test1(iterations);
            Test2(iterations);
        }
        private static void Test1(Int32 iterations)
        {
            Stopwatch sw = Stopwatch.StartNew();
            for (Int32 i = 0; i < iterations; i++)
            {
                UserBeforeFieldInit._name = "hlw_test1_UserBefore2017";
            }
            sw.Stop();
            Console.WriteLine("Test1-UserBeforeFieldInit 用时:" + sw.ElapsedMilliseconds);
            sw = Stopwatch.StartNew();
            for (Int32 j = 0; j < iterations; j++)
            {
                UserPrecise._name = "hlw_test1_UserPrecise2017";
            }
            sw.Stop();
            Console.WriteLine("Test1-UserPrecise 用时:" + sw.ElapsedMilliseconds);
        }
        private static void Test2(Int32 iterations)
        {
            Stopwatch sw = Stopwatch.StartNew();
            for (Int32 i = 0; i < iterations; i++)
            {
                UserBeforeFieldInit._name = "hlw_test2_UserBefore2017";
            }
            sw.Stop();
            Console.WriteLine("Test2-UserBeforeFieldInit 用时:" + sw.ElapsedMilliseconds);
            sw = Stopwatch.StartNew();
            for (Int32 j = 0; j < iterations; j++)
            {
                UserPrecise._name = "hlw_test2__UserPrecise2017";
            }
            sw.Stop();
            Console.WriteLine("Test2-UserPrecise 用时:" + sw.ElapsedMilliseconds);
        }
    }
}

输出结果为:
这里写图片描述

我们看下对应的IL源码:

.class public auto ansi beforefieldinit NowCoderProgrammingProject.UserBeforeFieldInit
       extends [mscorlib]System.Object
{
} // end of class NowCoderProgrammingProject.UserBeforeFieldInit

.class public auto ansi NowCoderProgrammingProject.UserPrecise
       extends [mscorlib]System.Object
{
} // end of class NowCoderProgrammingProject.UserPrecise

这里写图片描述

C#编译器如果看到一个类(BeforeFieldInit)包含进行了内联初始化的静态字段,会在类的类型定义表中生成一个添加了BeforeFiledInit元数据标记的记录项。C#编译器如果看到一个类包含显示的类型构造器,就不会添加BeforeFiledInit元数据标记。静态字段只要在访问之前初始化就可以了,具体什么时间无所谓。而显式类型构造器可能包含具有副作用的代码,所以需要在精确拿捏运行的时间

从输出结果来看,这个决定对性能影响很大。当Test1执行第一个循环时需要的时间为3411(根据不同的机器运行结果不同),第二个循环为5072.而二个循环里面的时间非常接近,因为JIT编译器知道类型的构造器已被调用,所以本地代码不需要包含任何对类型构造器方法的调用。
遗憾的是目前C#还不允许在源代码中设置BeforeFieldInit标志,而是让C#编译器根据类型构造器隐士还是显式创建来决定是否设置这个值。
所以我们最后根据需要慎重选择静态构造器
参考《CLR VIA C# 第四版》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值