[你必须知道的.NET]第二十三回:品味细节,深入.NET的类型构造器

Author:王涛

Date:2008-11-2

© 2008 Anytao.com ,Anytao原创作品,转贴请注明作者和出处。

说在,开篇之前

今天Artech 兄在《关于 Type Initializer和 BeforeFieldInit的问题,看看大家能否给出正确的解释 》一文中让我们认识了一个关于类型构造器调用执行的有趣示例,其中也相应提出了一些关于beforefieldinit 对于类型构造器调用时机的探讨,对于我们很好的理解类型构造器给出了一个很好的应用实践体验。
认识类型构造器,认识beforefieldinit ,更深入关注CLR 执行机理,品味细节之美。

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 ,类型实例化时会执行对应的构造器进行类型初始化的操作。
关于实例化的过程,设计到比较复杂的执行顺序,按照类型基础层次进行初始化的过程可以参阅《你必须知道的.NET 》7.8 节 “ 动静之间:静态和非静态” 一文中有详细的介绍和分析,本文中将不做过多探讨。
本文的重点以考察类型构造器为主,所以在此不进行过多探讨。

  • 类型构造器(.cctor )

用于执行对静态成员的初始化,在.NET 中,类型在两种情况下会发生对.cctor 的调用:

  • 为静态成员指定初始值,例如上例中只有静态成员初始化,而没有静态构造函数时,.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
  • 实现显式的静态构造函数,例如上例中有静态构造函数存在时,将首先执行静态成员的初始化过程,再执行静态构造函数初始化过程,.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 无法保证这多个构造器之间的执行顺序,因此我们在实际的编码时应该尽量避免这种情况的发生。

4 回归问题,必要的小结

本文源于Artech 兄的一个问题,希望通过上文的分析可以给出一点值得参考的背景。现在就关于 Type Initializer和 BeforeFieldInit的问题,看看大家能否给出正确的解释 一文中的几个示例进行一些继续的分析:

  • 在蒋兄的开始的示例实现中,可以很容易的来确定对于显式实现了静态构造函数的情况,类型构造器的调用在刚好引用静态成员之前发生,所以不管是否在Main 中声明

string field = Foo.Field;

执行的结果不受影响。

  • 而在没有显式实现静态构造函数的情况下,beforefieldinit 优化了类型构造器的执行不在确定的时间执行,只要实在静态成员引用或者类型实例发生之前即可,所以在Debug 环境下调用的时机变得不按常理。然而在Release 优化模式下,beforefieldinit 的执行顺序并不受

string field = Foo.Field;

的影响,完全符合beforefieldinit 优化执行的语义定义。

  • 关于最后一个静态成员继承情况的结果,正像本文开始描述的逻辑一样,类型构造器是在静态成员被调用或者创建实例时发生,所以示例的结果是完全遵守规范的。不过,我并不建议子类最好不要调用父类静态成员,原因是作为继承机制而言,子承父业是继承的基本规范,除了强制为private 之外,所有的成员或者方法都应在子类中可见。而对于存在的潜在问题,更好的以规范来约束可能会更好。其中,静态方法一定程度上是一种结构化的实现机制,在面向对象的继承关系中,本质上就存在一定的不足。
  • 在c# 规范中,关于beforefieldinit 的控制已经引起很多的关注和非议,一方面beforefieldinit 方式可以有效的优化调用性能,但是以显式和或者隐式实现静态构造函数的方式不能更有直观的让程序开发者来控制,因此在以后版本的c# 中,能实现基于特性的声明方式来控制,是值得期待的。
  • 另一方面,在有两个类型的类型构造器相互引用的情况下,CLR 无法保证类型构造器的调用顺序,对程序开发者而言,我同样强调了对于类型构造器而言,我们应该尽量避免要求顺序相关的业务逻辑,因为很多时候执行的顺序并非声明的顺序,这是值得关注的。

5 结论

除了补充Artech 老兄的问题,本文算是继续了关于类型构造器在《你必须知道的 .NET 》7.8 节 “ 动静之间:静态和非静态” 中的探讨,以更全面的视角来进一步阐释这个问题。在最后,关于beforefieldinit 标记引起的类型构造器调用优化的问题,虽然没有完全100% 的了解在Debug 模式下的CLR 调用行为,但是深入细节我们可以掌控对于语言之内更多的理解,从这点而言,本文是个开始。

 

 

 

下一篇:(王涛)[开发故事]第一回:用简单方式,实现多个可执行文件的合并和执行?