BeforeFieldInit 与类静态构造函数
罗朝辉 (http://kesalin.cnblogs.com/)
如下代码:
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 有静态的类构造函数。执行上面的代码,输出如下:
如果把被注释的读取静态字段Field的两行代码打开,再编译运行,输出:
对比上面的区别,FooStatic 始终是延迟装载的,也就是只有类被首次使用时,类对象才被构造,其静态成员以及静态构造函数才被初始化执行, 而 Foo 类对象的初始化则交给 CLR 来决定。
如果用 IL Dasm.exe对比两个类生成的中间代码,可以看到只有一处不同:FooStatic 比 Foo 少了一个特性:beforefieldinit。
也就是说静态构造函数抑制了 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 节中提到:
- 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可能会提前调用一个类型的静态构造函数,而precise模式是非要等到用时才调用类型的静态构造函数,它是严格的延迟装载。
beforefieldinit 是首选的(如果没有自定义静态构造函数,默认就是这种方式),因为它使CLR能够自由选择调用静态构造函数的时机,而CLR会尽可能利用这一点来生成运行得更快的代码。比如说在一个循环中调用单例(且包含首次调用),beforefieldinit方式可以让CLR决定在循环之前就调用静态构造函数来优化,而precise模式则只会在循环体中来调用静态构造函数,并在之后的调用会检测静态构造函数是否已被执行的标志位,这样效率稍低一些。在前面使用静态Field的情况下,beforefieldinit 方式下CLR也认为提前执行静态构造函数是更好的选择。
C# 的单例实现,可以利用 precise 延迟调用这一点来延迟对单例对象的构造(饿汗模式),从而带来一丁点的优化,但是在绝大部分情况下这一丁点的优化作用并不大!