最近看到一篇关于 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 有静态的类构造函数。执行上面的代码,输出如下:

[C#]BeforeFieldInit与类静态构造函数_第1张图片


  如果把被注释的读取静态字段Field的两行代码打开,再编译运行,输出:

[C#]BeforeFieldInit与类静态构造函数_第2张图片



对比上面的区别,FooStatic 始终是延迟装载的,也就是只有类被首次使用时,类对象才被构造,其静态成员以及静态构造函数才被初始化执行,而Foo 类对象的初始化则交给CLR 来决定。
如果用IL Dasm.exe对比两个类生成的中间代码,可以看到只有一处不同:FooStatic 比Foo 少了一个特性:

[C#]BeforeFieldInit与类静态构造函数_第3张图片


也就是说静态构造函数抑制了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 延迟调用这一点来延迟对单例对象的构造(饿汗模式),从而带来一丁点的优化,但是在绝大部分情况下这一丁点的优化作用并不大!