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

《你必须知道的.NET》网站 | Anytao技术博客 

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

发布日期:2008.11.2 作者:Anytao
© 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调用行为,但是深入细节我们可以掌控对于语言之内更多的理解,从这点而言,本文是个开始。

 

anytao | © 2008 Anytao.com

2008/11/02 | 荣誉出品:http://www.cnblogs.com/anytao

本文以“现状”提供且没有任何担保,同时也没有授予任何权利。 | This posting is provided "AS IS" with no warranties, and confers no rights.

本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

 

参考文献

  • 你必须知道的.NET》7.8节 “动静之间:静态和非静态”
  • Artech关于Type Initializer和 BeforeFieldInit的问题,看看大家能否给出正确的解释
  • 通过七个关键编程技巧得益于静态内容

    温故知新

    [开篇有益]
    [第一回:恩怨情仇:is和as]
    [第二回:对抽象编程:接口和抽象类]
    [第三回:历史纠葛:特性和属性]
    [第四回:后来居上:class和struct]
    [第五回:深入浅出关键字---把new说透]
    [第六回:深入浅出关键字---base和this]
    [第七回:品味类型---从通用类型系统开始]
    [第八回:品味类型---值类型与引用类型(上)-内存有理]
    [第九回:品味类型---值类型与引用类型(中)-规则无边]
    [第十回:品味类型---值类型与引用类型(下)-应用征途]
    [第十一回:参数之惑---传递的艺术(上)]
    [第十二回:参数之惑---传递的艺术(下)]
    [第十三回:从Hello, world开始认识IL]
    [第十四回:认识IL代码---从开始到现在]
    [第十五回:继承本质论]
    [第十六回:深入浅出关键字---using全接触]
    [第十七回:貌合神离:覆写和重载]
    [第十八回:对象创建始末(上)]
    [第十九回:对象创建始末(下)]
    [第二十回:学习方法论]
    [第二十一回:认识全面的null]
    [第二十二回:字符串驻留(上)---带着问题思考]

  • posted @ 2008-11-02 02:53  Anytao  阅读(8047)  评论(57编辑  收藏