今天Artech兄在《关于Type Initializer和 BeforeFieldInit的问题,看看大家能否给出正确的解释》一文中让我们认识了一个关于类型构造器调用执行的有趣示例,其中也相应提出了一些关于beforefieldinit对于类型构造器调用时机的探讨,对于我们很好的理解类型构造器给出了一个很好的应用实践体验。
作为补充,本文希望从基础开始再层层深入,把《关于Type Initializer和 BeforeFieldInit的问题,看看大家能否给出正确的解释》一文中没有解释的概念和原理,进行必要的补充,例如更全面的认识类型构造器,认识BeforeFieldInit。并在此基础上,探讨一点关于类型构造器的实践应用,同时期望能够回答其中示例运行的结果。
废话少说,我们开始。
2 认识对象构造器和类型构造器
在.NET中,一个类的初始化过程是在构造器中进行的。并且根据构造成员的类型,分为类型构造器(.cctor)和对象构造器(.ctor), 其中.cctor和.ctor为二者在IL代码中的指令表示。.cctor不能被直接调用,其调用规则正是本文欲加阐述的重点,详见后文的分析;而.ctor会在类型实例化时被自动调用。
基于对类型构造器的探讨,我们有必要首先实现一个简单的类定义,其中包括普通的构造器和静态构造器,例如
<span style="color:black"><span style="color:black"> <span style="color:#008000">// Release : code01, 2008/11/02 </span></span></span>
// Author : Anytao, http://www.anytao.com
<span style="color:black"><span style="color:black"> <span style="color:#0000ff">public</span> <span style="color:#0000ff">class</span> User</span></span>
{
<span style="color:black"><span style="color:black"> <span style="color:#0000ff">static</span> User()</span></span>
{
<span style="color:black"><span style="color:black"> message = <span style="color:#006080">"Initialize in static constructor."</span>;</span></span>
}
<span style="color:black"><span style="color:black"> </span></span>
public User()
<span style="color:black"><span style="color:black"> {</span></span>
message = "Initialize in normal construcotr.";
<span style="color:black"><span style="color:black"> }</span></span>
<span style="color:black"><span style="color:black"> <span style="color:#0000ff">public</span> User(<span style="color:#0000ff">string</span> name, <span style="color:#0000ff">int</span> age)</span></span>
{
<span style="color:black"><span style="color:black"> Name = name;</span></span>
Age = age;
<span style="color:black"><span style="color:black"> }</span></span>
<span style="color:black"><span style="color:black"> <span style="color:#0000ff">public</span> <span style="color:#0000ff">string</span> Name { get; set; }</span></span>
<span style="color:black"><span style="color:black"> <span style="color:#0000ff">public</span> <span style="color:#0000ff">int</span> Age { get; set; }</span></span>
<span style="color:black"><span style="color:black"> <span style="color:#0000ff">public</span> <span style="color:#0000ff">static</span> <span style="color:#0000ff">string</span> message = <span style="color:#006080">"Initialize when defined."</span>;</span></span>
我们将上述代码使用ILDasm.exe工具反编译为IL代码,可以很方便的找到相应的类型构造器和对象构造器的影子,如图
然后,我们简单的来了解一下对象构造器和类型构造器的概念。
- 对象构造器(.ctor)
在生成的IL代码中将可以看到对应的ctor,类型实例化时会执行对应的构造器进行类型初始化的操作。
关于实例化的过程,设计到比较复杂的执行顺序,按照类型基础层次进行初始化的过程可以参阅《你必须知道的.NET》7.8节 “动静之间:静态和非静态”一文中有详细的介绍和分析,本文中将不做过多探讨。
本文的重点以考察类型构造器为主,所以在此不进行过多探讨。
- 类型构造器(.cctor)
用于执行对静态成员的初始化,在.NET中,类型在两种情况下会发生对.cctor的调用:
- 为静态成员指定初始值,例如上例中只有静态成员初始化,而没有静态构造函数时,.cctor的IL代码实现为:
<span style="color:black"><span style="color:black">.method <span style="color:#0000ff">private</span> hidebysig specialname rtspecialname <span style="color:#0000ff">static</span> </span></span>
void .cctor() cil managed
<span style="color:black"><span style="color:black">{</span></span>
// Code size 11 (0xb)
<span style="color:black"><span style="color:black"> .maxstack 8</span></span>
IL_0000: ldstr "Initialize when defined."
<span style="color:black"><span style="color:black"> IL_0005: stsfld <span style="color:#0000ff">string</span> Anytao.Write.TypeInit.User::message</span></span>
IL_000a: ret
<span style="color:black"><span style="color:black">} <span style="color:#008000">// end of method User::.cctor</span></span></span>
- 实现显式的静态构造函数,例如上例中有静态构造函数存在时,将首先执行静态成员的初始化过程,再执行静态构造函数初始化过程,.cctor的IL代码实现为:
<span style="color:black"><span style="color:black">.method <span style="color:#0000ff">private</span> hidebysig specialname rtspecialname <span style="color:#0000ff">static</span> </span></span>
void .cctor() cil managed
<span style="color:black"><span style="color:black">{</span></span>
// Code size 23 (0x17)
<span style="color:black"><span style="color:black"> .maxstack 8</span></span>
IL_0000: ldstr "Initialize when defined."
<span style="color:black"><span style="color:black"> IL_0005: stsfld <span style="color:#0000ff">string</span> Anytao.Write.TypeInit.User::message</span></span>
IL_000a: nop
<span style="color:black"><span style="color:black"> IL_000b: ldstr <span style="color:#006080">"Initialize in static constructor."</span></span></span>
IL_0010: stsfld string Anytao.Write.TypeInit.User::message
<span style="color:black"><span style="color:black"> IL_0015: nop</span></span>
IL_0016: ret
<span style="color:black"><span style="color:black">} <span style="color:#008000">// end of method User::.cctor</span></span></span>
同时,我们必须明确一些静态构造函数的基本规则,包括:
- 必须为静态无参构造函数,并且一个类只能有一个。
- 只能对静态成员进行初始化。
- 静态无参构造函数可以和非静态无参构造函数共存,区别在于二者的执行时间,详见《你必须知道的.NET》7.8节 “动静之间:静态和非静态”的论述,其他更多的区别和差异也详见本节的描述。
3 深入执行过程
因为类型构造器本身的特点,在一定程度上决定了.cctor的调用时机并非是一个确定的概念。因为类型构造器都是private的,用户不能显式调用类型构造器。所以关于类型构造器的执行时机问题在.NET中主要包括两种方案:
- precise方式
- beforefieldinit方式
二者的执行差别主要体现在是否为类型实现了显式的静态构造函数,如果实现了显式的静态构造函数,则按照precise方式执行;如果没有实现显式的静态构造函数,则按照beforefieldinit方式执行。
为了说清楚类型构造器的执行情况,我们首先在概念上必须明确一个前提,那就是precise的语义明确了.cctor的调用和调用存取静态成员的时机存在精确的关系,所以换句话说,类型构造器的执行时机在语义上决定于是否显式的声明了静态构造函数,以及存取静态成员的时机,这两个因素。
我们还是从User类的实现说起,一一过招分析这两种方式的执行过程。
3.1 precise方式
首先实现显式的静态构造函数方案,为:
<span style="color:black"><span style="color:black"> <span style="color:#008000">// Release : code02, 2008/11/02 </span></span></span>
// Author : Anytao, http://www.anytao.com
<span style="color:black"><span style="color:black"> <span style="color:#0000ff">public</span> <span style="color:#0000ff">class</span> User</span></span>
{
<span style="color:black"><span style="color:black"> <span style="color:#008000">//Explicit Constructor</span></span></span>
static User()
<span style="color:black"><span style="color:black"> {</span></span>
message = "Initialize in static constructor.";
<span style="color:black"><span style="color:black"> }</span></span>
<span style="color:black"><span style="color:black"> <span style="color:#0000ff">public</span> <span style="color:#0000ff">static</span> <span style="color:#0000ff">string</span> message = <span style="color:#006080">"Initialize when defined."</span>;</span></span>
}
对应的IL代码为:
<span style="color:black"><span style="color:black">.<span style="color:#0000ff">class</span> <span style="color:#0000ff">public</span> auto ansi User</span></span>
extends [mscorlib]System.Object
<span style="color:black"><span style="color:black">{</span></span>
.method private hidebysig specialname rtspecialname static void .cctor() cil managed
<span style="color:black"><span style="color:black"> {</span></span>
.maxstack 8
<span style="color:black"><span style="color:black"> L_0000: ldstr <span style="color:#006080">"Initialize when defined."</span></span></span>
L_0005: stsfld string Anytao.Write.TypeInit.User::message
<span style="color:black"><span style="color:black"> L_000a: nop </span></span>
L_000b: ldstr "Initialize in static constructor."
<span style="color:black"><span style="color:black"> L_0010: stsfld <span style="color:#0000ff">string</span> Anytao.Write.TypeInit.User::message</span></span>
L_0015: nop
<span style="color:black"><span style="color:black"> L_0016: ret </span></span>
}
<span style="color:black"><span style="color:black"> </span></span>
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
<span style="color:black"><span style="color:black"> {</span></span>
.maxstack 8
<span style="color:black"><span style="color:black"> L_0000: ldarg.0 </span></span>
L_0001: call instance void [mscorlib]System.Object::.ctor()
<span style="color:black"><span style="color:black"> L_0006: ret </span></span>
}
<span style="color:black"><span style="color:black"> </span></span>
.field public static string message
<span style="color:black"><span style="color:black">}</span></span>
为了进行对比分析,我们需要首先分析beforefieldinit方式的执行情况,所以接着继续。。。
3.2 beforefieldinit方式
为User类型,不实现显式的静态构造函数方案,为:
<span style="color:black"><span style="color:black"> <span style="color:#008000">// Release : code03, 2008/11/02 </span></span></span>
// Author : Anytao, http://www.anytao.com
<span style="color:black"><span style="color:black"> <span style="color:#0000ff">public</span> <span style="color:#0000ff">class</span> User</span></span>
{
<span style="color:black"><span style="color:black"> <span style="color:#008000">//Implicit Constructor</span></span></span>
public static string message = "Initialize when defined.";
<span style="color:black"><span style="color:black"> }</span></span>
对应的IL代码为:
<span style="color:black"><span style="color:black">.<span style="color:#0000ff">class</span> <span style="color:#0000ff">public</span> auto ansi beforefieldinit User</span></span>
extends [mscorlib]System.Object
<span style="color:black"><span style="color:black">{</span></span>
.method private hidebysig specialname rtspecialname static void .cctor() cil managed
<span style="color:black"><span style="color:black"> {</span></span>
.maxstack 8
<span style="color:black"><span style="color:black"> L_0000: ldstr <span style="color:#006080">"Initialize when defined."</span></span></span>
L_0005: stsfld string Anytao.Write.TypeInit.User::message
<span style="color:black"><span style="color:black"> L_000a: ret </span></span>
}
<span style="color:black"><span style="color:black"> </span></span>
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
<span style="color:black"><span style="color:black"> {</span></span>
.maxstack 8
<span style="color:black"><span style="color:black"> L_0000: ldarg.0 </span></span>
L_0001: call instance void [mscorlib]System.Object::.ctor()
<span style="color:black"><span style="color:black"> L_0006: ret </span></span>
}
<span style="color:black"><span style="color:black"> </span></span>
.field public static string message
<span style="color:black"><span style="color:black">}</span></span>
3.3 分析差别
从IL代码的执行过程而言,我们首先可以了解的是在显式和隐式实现类型构造函数的内部,除了添加新的初始化操作之外,二者的实现是基本相同的。所以要找出两种方式的差别,我们最终将着眼点锁定在二者元数据的声明上,隐式方式多了一个称为beforefieldinit标记的指令。
那么,beforefieldinit究竟表示什么样的语义呢?Scott Allen对此进行了详细的解释:beforefieldinit为CLR提供了在任何时候执行.cctor的授权,只要该方法在第一次访问类型的静态字段之前执行即可。
所以,如果对precise方式和beforefieldinit方式进行比较时,二者的差别就在于是否在元数据声明时标记了beforefieldinit指令。precise方式下,CLR必须在第一次访问该类型的静态成员或者实例成员之前执行类型构造器,也就是说必须刚好在存取静态成员或者创建实例成员之前完成类型构造器的调用;beforefieldinit方式下,CLR可以在任何时候执行类型构造器,一定程度上实现了对执行性能的优化,因此较precise方式更加高效。
值得注意的是,当有多个beforefieldinit构造器存在时,CLR无法保证这多个构造器之间的执行顺序,因此我们在实际的编码时应该尽量避免这种情况的发生。
4 回归问题,必要的小结
本文源于Artech兄的一个问题,希望通过上文的分析可以给出一点值得参考的背景。现在就关于Type Initializer和 BeforeFieldInit的问题,看看大家能否给出正确的解释一文中的几个示例进行一些继续的分析:
- 在蒋兄的开始的示例实现中,可以很容易的来确定对于显式实现了静态构造函数的情况,类型构造器的调用在刚好引用静态成员之前发生,所以不管是否在Main中声明
<span style="color:black"><span style="color:black"><span style="color:#0000ff">string</span> field = Foo.Field;</span></span>
执行的结果不受影响。
- 而在没有显式实现静态构造函数的情况下,beforefieldinit优化了类型构造器的执行不在确定的时间执行,只要实在静态成员引用或者类型实例发生之前即可,所以在Debug环境下调用的时机变得不按常理。然而在Release优化模式下,beforefieldinit的执行顺序并不受
<span style="color:black"><span style="color:black"><span style="color:#0000ff">string</span> field = Foo.Field;</span></span>
的影响,完全符合beforefieldinit优化执行的语义定义。
- 关于最后一个静态成员继承情况的结果,正像本文开始描述的逻辑一样,类型构造器是在静态成员被调用或者创建实例时发生,所以示例的结果是完全遵守规范的。不过,我并不建议子类最好不要调用父类静态成员,原因是作为继承机制而言,子承父业是继承的基本规范,除了强制为private之外,所有的成员或者方法都应在子类中可见。而对于存在的潜在问题,更好的以规范来约束可能会更好。其中,静态方法一定程度上是一种结构化的实现机制,在面向对象的继承关系中,本质上就存在一定的不足。
- 在c#规范中,关于beforefieldinit的控制已经引起很多的关注和非议,一方面beforefieldinit方式可以有效的优化调用性能,但是以显式和或者隐式实现静态构造函数的方式不能更有直观的让程序开发者来控制,因此在以后版本的c#中,能实现基于特性的声明方式来控制,是值得期待的。
- 另一方面,在有两个类型的类型构造器相互引用的情况下,CLR无法保证类型构造器的调用顺序,对程序开发者而言,我同样强调了对于类型构造器而言,我们应该尽量避免要求顺序相关的业务逻辑,因为很多时候执行的顺序并非声明的顺序,这是值得关注的。
5 结论
除了补充Artech老兄的问题,本文算是继续了关于类型构造器在《你必须知道的.NET》7.8节 “动静之间:静态和非静态”中的探讨,以更全面的视角来进一步阐释这个问题。在最后,关于beforefieldinit标记引起的类型构造器调用优化的问题,虽然没有完全100%的了解在Debug模式下的CLR调用行为,但是深入细节我们可以掌控对于语言之内更多的理解,从这点而言,本文是个开始。
Worktile,新一代简单好用、体验极致的团队协同、项目管理工具,让你和你的团队随时随地一起工作。完全免费,现在就去了解一下吧。
https://worktile.com
参考文献
- 《你必须知道的.NET》7.8节 “动静之间:静态和非静态”
- Artech,关于Type Initializer和 BeforeFieldInit的问题,看看大家能否给出正确的解释
- 通过七个关键编程技巧得益于静态内容
-
#53楼 2009-10-27 22:25 fisea
请教lz一个问题:
如下程序代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class
Program
{
static
void
Main()
{
Console.WriteLine(
"Start ..."
);
Foo.GetString(
"Manually invoke the static GetString() method!"
);
}
}
class
Foo
{
public
static
string
Field = GetString(
"Initialize the static field!"
);
public
static
string
GetString(
string
s)
{
Console.WriteLine(s);
return
s;
}
}
的运行结果如下:
Start ...
Initialize the static field!
Manually invoke the static GetString() method!
和如下代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class
Program
{
static
void
Main()
{
Console.WriteLine(
"Main execute!"
);
Console.WriteLine(
"int: "
+ MyClass<
int
>.Time);
Thread.Sleep(3000);
Console.WriteLine(
"string: "
+ MyClass<
string
>.Time);
Console.ReadLine();
}
}
public
static
class
MyClass<T>
{
public
static
readonly
DateTime Time = GetNow();
private
static
DateTime GetNow()
{
Console.WriteLine(
"GetNow execute!"
);
return
DateTime.Now;
}
}
的运行结果:
GetNow execute!
GetNow execute!
Main execute!
int: 2009-10-27 22:20:06
string: 2009-10-27 22:20:06
问题:
1、第一个例子为什么先执行Main中的Console.WriteLine("Start ...");
而第二个例子是先执行MyClass类中GetNow()函数。
2、第二个例子中的运行结果中为什么有两个GetNow execute!
GetNow execute!。请教lz。谢谢。支持(0) 反对(0)
#54楼 2010-05-05 22:12 Edenia
LZ能否回答一下53楼的问题,我也感觉很迷茫,O(∩_∩)O谢谢了~
支持(0) 反对(0)
#55楼 2010-11-29 21:05 李董
引用fisea:请教lz一个问题:
如下程序代码:
[code=csharp]
class Program
{
static void Main()
{
Console.WriteLine("Start ...");
Foo.GetString("Manually invoke the static GetString() method!");
}
}
class Foo
{
public ...
第一段代码中运行到Foo.GetString("Manually invoke the static GetString() method!");这句时,会先执行该类的静态变量和静态构造函数(此处未显示定义静态构造函数),静态变量初始化后再执行上面的这句方法。支持(0) 反对(0)
#56楼 2011-02-16 16:14 王磊的博客
晚上睡觉不 哥,不累啊,自娱自乐的疯狂技术侠客,佩服!
支持(0) 反对(0)
#57楼 2013-07-27 11:50 String.Trim()
@ Edenia
@fisea
1.第二段代码中有
Console.WriteLine("int: " + MyClass<int>.Time);
Console.WriteLine("string: " + MyClass<string>.Time);
对Time静态成员进行引用,
而MyClass<T>类中没有类型(静态)构造器,所以CLR可以在任何时候执行类型构造器,只要是在静态成员引用或者类型实例发生之前(不包括静态方法)即可。会在Main方法之前执行。
第一段代码没有对静态成员进行引用,所以会按照顺序执行代码。
2.泛型类只有在具有相同类型形参的类型的实例才能够共享同一个静态字段的值。MyClass<int>与MyClass<string>中的静态字段值是不同的,所以GetNow()会执行2遍。