[C#]BeforeFieldInit与类静态构造函数

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

wKioL1j9bJmwWYkZAAAS97GYgKA327.png


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

wKioL1j9bKWQ-6HdAAAQQaV0zHQ888.png



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

wKioL1jYeIbwEJmDAABdz1IunUs999.png


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

相关文章
相关标签/搜索