品味细节,深入.NET的类型构造器
转载自:http://msdn.microsoft.com/zh-cn/dd368012.aspx
1 引言
今天Artech 兄在《关于 Type Initializer和 BeforeFieldInit的问题,看看大家能否给出正确的解释 》一文中让我们认识了一个关于类型构造器调用执行的有趣示例,其中也相应提出了一些关于beforefieldinit 对于类型构造器调用时机的探讨,对于我们很好的理解类型构造器给出了一个很好的应用实践体验。
作为补充,本文希望从基础开始再层层深入,把《关于Type Initializer 和 BeforeFieldInit 的问题,看看大家能否给出正确的解释》一文中没有解释的概念和原理,进行必要的补充,例如更全面的认识类型构造器,认识BeforeFieldInit 。并在此基础上,探讨一点关于类型构造器的实践应用,同时期望能够回答其中示例运行的结果。
废话少说,我们开始。
2 认识对象构造器和类型构造器
在.NET 中,一个类的初始化过程是在构造器中进行的。并且根据构造成员的类型,分为类型构造器(.cctor )和对象构造器(.ctor ), 其中.cctor 和.ctor 为二者在IL 代码中的指令表示。.cctor 不能被直接调用,其调用规则正是本文欲加阐述的重点,详见后文的分析;而.ctor 会在类型实例化时被自动调用。
基于对类型构造器的探讨,我们有必要首先实现一个简单的类定义,其中包括普通的构造器和静态构造器,例如
- // Release : code01, 2008/11/02
- // Author : Anytao, http://www.anytao.com
- public class User
- {
- static User()
- {
- message = "Initialize in static constructor.";
- }
- public User()
- {
- message = "Initialize in normal construcotr.";
- }
- public User(string name, int age)
- {
- Name = name;
- Age = age;
- }
- public string Name { get; set; }
- public int Age { get; set; }
- public static string message = "Initialize when defined.";
我们将上述代码使用ILDasm.exe 工具反编译为IL 代码,可以很方便的找到相应的类型构造器和对象构造器的影子,如图
然后,我们简单的来了解一下对象构造器和类型构造器的概念。
- 对象构造器(.ctor )
在生成的IL 代码中将可以看到对应的ctor ,类型实例化时会执行对应的构造器进行类型初始化的操作。
- 类型构造器(.cctor )
用于执行对静态成员的初始化,在.NET 中,类型在两种情况下会发生对.cctor 的调用:
1. 为静态成员指定初始值,例如上例中只有静态成员初始化,而没有静态构造函数时,.cctor 的IL 代码实现为:
- .method private hidebysig specialname rtspecialname static
- void .cctor() cil managed
- {
- // Code size 11 (0xb)
- .maxstack 8
- IL_0000: ldstr "Initialize when defined."
- IL_0005: stsfld string Anytao.Write.TypeInit.User::message
- IL_000a: ret
- } // end of method User::.cctor
2. 实现显式的静态构造函数,例如上例中有静态构造函数存在时,将首先执行静态成员的初始化过程,再执行静态构造函数初始化过程,.cctor 的IL 代码实现为:
- .method private hidebysig specialname rtspecialname static
- void .cctor() cil managed
- {
- // Code size 23 (0x17)
- .maxstack 8
- IL_0000: ldstr "Initialize when defined."
- IL_0005: stsfld string Anytao.Write.TypeInit.User::message
- IL_000a: nop
- IL_000b: ldstr "Initialize in static constructor."
- IL_0010: stsfld string Anytao.Write.TypeInit.User::message
- IL_0015: nop
- IL_0016: ret
- } // end of method User::.cctor
同时,我们必须明确一些静态构造函数的基本规则,包括:
- 必须为静态无参构造函数,并且一个类只能有一个。
- 只能对静态成员进行初始化。
- 静态无参构造函数可以和非静态无参构造函数共存,区别在于二者的执行时间,详见《你必须知道的.NET 》7.8 节 “ 动静之间:静态和非静态” 的论述,其他更多的区别和差异也详见本节的描述。
3 深入执行过程
因为类型构造器本身的特点,在一定程度上决定了.cctor 的调用时机并非是一个确定的概念。因为类型构造器都是private 的,用户不能显式调用类型构造器。所以关于类型构造器的执行时机问题在.NET 中主要包括两种方案:
- precise 方式
- beforefieldinit 方式
二者的执行差别主要体现在是否为类型实现了显式的静态构造函数,如果实现了显式的静态构造函数,则按照precise 方式执行;如果没有实现显式的静态构造函数,则按照beforefieldinit 方式执行。
为了说清楚类型构造器的执行情况,我们首先在概念上必须明确一个前提,那就是precise 的语义明确了.cctor 的调用和调用存取静态成员的时机存在精确的关系,所以换句话说,类型构造器的执行时机在语义上决定于是否显式的声明了静态构造函数,以及存取静态成员的时机,这两个因素。
我们还是从User 类的实现说起,一一过招分析这两种方式的执行过程。
3.1 precise 方式
首先实现显式的静态构造函数方案,为:
- // Release : code02, 2008/11/02
- // Author : Anytao, http://www.anytao.com
- public class User
- {
- //Explicit Constructor
- static User()
- {
- message = "Initialize in static constructor.";
- }
- public static string message = "Initialize when defined.";
- }
对应的IL 代码为:
- .class public auto ansi User
- extends [mscorlib]System.Object
- {
- .method private hidebysig specialname rtspecialname static void .cctor() cil managed
- {
- .maxstack 8
- L_0000: ldstr "Initialize when defined."
- L_0005: stsfld string Anytao.Write.TypeInit.User::message
- L_000a: nop
- L_000b: ldstr "Initialize in static constructor."
- L_0010: stsfld string Anytao.Write.TypeInit.User::message
- L_0015: nop
- L_0016: ret
- }
- .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
- {
- .maxstack 8
- L_0000: ldarg.0
- L_0001: call instance void [mscorlib]System.Object::.ctor()
- L_0006: ret
- }
- .field public static string message
- }
为了进行对比分析,我们需要首先分析beforefieldinit 方式的执行情况,所以接着继续。。。
3.2 beforefieldinit 方式
为User 类型,不实现显式的静态构造函数方案,为:
- // Release : code03, 2008/11/02
- // Author : Anytao, http://www.anytao.com
- public class User
- {
- //Implicit Constructor
- public static string message = "Initialize when defined.";
- }
- 对应的IL代码为:
- .class public auto ansi beforefieldinit User
- extends [mscorlib]System.Object
- {
- .method private hidebysig specialname rtspecialname static void .cctor() cil managed
- {
- .maxstack 8
- L_0000: ldstr "Initialize when defined."
- L_0005: stsfld string Anytao.Write.TypeInit.User::message
- L_000a: ret
- }
- .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
- {
- .maxstack 8
- L_0000: ldarg.0
- L_0001: call instance void [mscorlib]System.Object::.ctor()
- L_0006: ret
- }
- .field public static string message
- }
3.3 分析差别
从IL 代码的执行过程而言,我们首先可以了解的是在显式和隐式实现类型构造函数的内部,除了添加新的初始化操作之外,二者的实现是基本相同的。所以要找出两种方式的差别,我们最终将着眼点锁定在二者元数据的声明上,隐式方式多了一个称为beforefieldinit 标记的指令。
那么,beforefieldinit 究竟表示什么样的语义呢?Scott Allen 对此进行了详细的解释:beforefieldinit 为CLR 提供了在任何时候执行.cctor 的授权,只要该方法在第一次访问类型的静态字段之前执行即可。
所以,如果对precise 方式和beforefieldinit 方式进行比较时,二者的差别就在于是否在元数据声明时标记了beforefieldinit 指令。precise 方式下,CLR 必须在第一次访问该类型的静态成员或者实例成员之前执行类型构造器,也就是说必须刚好在存取静态成员或者创建实例成员之前完成类型构造器的调用;beforefieldinit 方式下,CLR 可以在任何时候执行类型构造器,一定程度上实现了对执行性能的优化,因此较precise 方式更加高效。
值得注意的是,当有多个beforefieldinit 构造器存在时,CLR 无法保证这多个构造器之间的执行顺序,因此我们在实际的编码时应该尽量避免这种情况的发生。
备注:
假设有下面的一个类
class MyTest
{
public static int Number = 1;
public MyTest()
{
Number++;
}
}
被下面的方法调用
private void Test()
{
var t1 = new MyTest();
t1 = null;
var t2 = new MyTest();
}
这里有三行代码,下面是不同时间MyTest.Number值的变化:
时间 MyTest.Number的值
第一行执行前 0
第一行执行后 2
第二行执行后 2(即使实例被置为null,但是静态值还是常驻内存的)
第三行执行后 3