代码改变世界

【读书笔记】.NET本质论第三章-Type Basics(Part 2)

2009-07-15 20:46  横刀天笑  阅读(1082)  评论(3编辑  收藏  举报

       在上一篇文章我们主要讨论了实例成员。顾名思义,实例成员属于类型的每个实例。对于实例字段来说,有n个实例,就得在内存中分配这n个实例字段。其实除了这里的实例成员,还有一类成员是属于类型的,它们被所有的实例所共享。在C#里我们使用static关键字标识某个成员是一个类型成员(在VB.NET里有更恰当的关键字:Shared)。

定义静态字段(C#):

static int _count;

定义静态方法(C#):

   1: static void StaticMethod()
   2: {
   3:  //do something
   4: }

注意:虽然这里使用“静态”这个术语,但这里的静态和C语言里的静态的意义并不一样,这要注意区分。

要访问一个静态的成员,我们必须直接通过类型名.成员名的形式访问,而不能使用实例名.成员名这种访问实例成员的方式,这个和其他一些语言是不同的。

因为静态的方法属于类型而不是实例,所以在静态方法里不能使用this关键字,因为这是一个静态的方法,它无从知道你的this指代的到底是哪一个实例。所以也不能直接访问实例成员,要访问实例成员必须先实例化类型,即使该类型就是该方法所在的类型也不行。

比如:

public class Foo
{
 static int _count; 
 public int _userId;
 static void StaticMethod()
 {
     //这样是可以的
     _count = 5;
     //这样是错误的
     _userId = 1;
     //应该这样才能访问,即使StaticMethod和_userId都同属于Foo类型
     Foo f = new Foo();
     f._userId = 5;
 }
}

静态类

有时,我们需要这样一种类型:它没有什么清晰的领域职责,它所做的就是一些辅助工作,比如字符串的转换,电话号码的验证等等。我们通常将这类类型命名为:

xxxHelper,xxxManager(我们称这类类型为管理者类型)

而且这类类型通常只包括一堆静态的方法,不包含任何实例方法或字段。对于这类类型我们没有必要实例化它,实例化也是不符合逻辑的。

在.net 1.x中很多人想出这样一种方法阻止客户代码实例化这类类型:

//抽象类,不允许实例化

   1: public abstract class ConfigurationManager
   2: { 
   3: //私有的构造函数,阻止子类继承 
   4:  private ConfigurationManager(){} 
   5:  //很多静态方法 
   6:  public static void StaticMethod(){//...}
   7: }

如此别扭的而且变态的设计真是令人哭笑不得。

幸好,.net2.0中加入了静态类:

public static class ConfigurationManager()
{}

记住哦,现在static关键字还可用在类型上。这样一来,不仅有了不可实例化,不可派生的功能,还具有暗示的作用,比上面那个变态的设计好多了。

实际上,有心的你可以使用ILDasm反编译出IL代码一探究竟:

变态的写法的IL:

.class public abstract auto ansi beforefieldinit ConfigurationManager extends [mscorlib]System.Object 
{ 
}

.net 2.0静态类的写法:

.class public abstract auto ansi sealed beforefieldinit ConfigurationManager extends [mscorlib]System.Object 
{ 
}

如果不是仔细观察,还还真是没发现有什么区别,原来.net 2.0的静态类也是一个抽象类啊,这是为了阻止客户代码实例化,而且还多了一个sealed,这是阻止子类从这个类继承。在“变态”写法中,我们用私有的构造器达到了这个目的。

注意:从这里看来,abstract与sealed在IL里是可以共存的,只是C#编译器做了检查,不允许一个类既定义为abstract的,又定义为sealed的(实际上,我倒觉得不可以共存是合理的,可以共存是不符合逻辑的,不过如果不能共存2.0又咋样实现这个静态类呢,呵呵)。

静态构造器(也称之为类型构造器或类型初始化器,以下就称为静态构造器吧)

先来看看静态构造器的一些特征:

1:名称为.cctor

2:没有返回值,没有参数并且是私有的

3:不能被别的代码直接调用,在类型初始化时由CLR自动调用

4:一个类型最多只能有一个静态构造器(根据1和2,名字相同,还没有返回值和参数,也不可能定义多个重载,呵呵)

5:静态构造器只被调用一次,不过调用的时刻却有些微妙的地方,这就是下面要关心的内容。

 

在上面的IL中,有心的你也许看到了这个元数据:beforefieldinit。我们先不解释这个东东,先来看看下面两个代码:

代码1:

public class Foo
{
 static int _count = 0;
}

代码2:

   1: public class Foo
   2: {
   3:     static int _count = 0;
   4:     //注意静态构造器定义的方式
   5:     static Foo()
   6:     {}
   7: }

仔细比较,代码1和代码2的区别就是代码1没有静态构造器,而代码2有。

这点细微的差别有什么影响呢?还是先反编译出IL:

代码1的IL代码:

   1: //类型的定义
   2: .class public auto ansi beforefieldinit Foo extends [mscorlib]System.Object 
   3: { 
   4: } 
   5: //静态的字段
   6: .field private static int32 _count 
   7: //静态构造器
   8: .method private hidebysig specialname rtspecialname static 
   9:  void .cctor() cil managed 
  10: { 
  11:     // Code size 7 (0x7) 
  12:     .maxstack 8 
  13:     IL_0000: ldc.i4.0 
  14:     IL_0001: stsfld int32 Foo::_count 
  15:     IL_0006: ret 
  16: } 
  17: //实例构造器
  18: .method public hidebysig specialname rtspecialname 
  19: instance void .ctor() cil managed 
  20: { 
  21:     // Code size 7 
  22:     (0x7) 
  23:     .maxstack 8 
  24:     IL_0000: ldarg.0 
  25:     IL_0001: call instance void [mscorlib]System.Object::.ctor() 
  26:     IL_0006: ret 
  27: } 

代码2的IL代码:

   1: .class public auto ansi Foo extends [mscorlib]System.Object 
   2: { 
   3: } 
   4: .field private static int32 _count 
   5: .method private hidebysig specialname rtspecialname static 
   6:  void .cctor() cil managed 
   7: { 
   8:     // Code size 9 (0x9) 
   9:     .maxstack 8 
  10:     IL_0000: ldc.i4.0 
  11:     IL_0001: stsfld int32 Foo::_count 
  12:     IL_0006: nop 
  13:     IL_0007: nop 
  14:     IL_0008: ret 
  15: } 
  16: .method public hidebysig specialname rtspecialname 
  17:  instance void .ctor() cil managed 
  18: { 
  19:     // Code size 7 
  20:     (0x7) 
  21:     .maxstack 8 
  22:     IL_0000: ldarg.0 
  23:     IL_0001: call instance void 
  24:     [mscorlib]System.Object::.ctor() 
  25:     IL_0006: ret 
  26: } 

这一比较不要紧,虽然代码1比代码2的C#代码少一个静态构造器,居然反编译后的IL代码是代码1比代码2多一个beforefieldinit,其余的一模一样。

我们再来看看下面的代码:

public class Foo{}

一个空的类型,反编译:

   1: .class public auto ansi beforefieldinit Foo 
   2:  extends [mscorlib]System.Object 
   3: { 
   4: }
   5: .method public hidebysig specialname rtspecialname 
   6:  instance void .ctor() cil managed 
   7: { 
   8:  // Code size 7 
   9: (0x7) 
  10:  .maxstack 8 
  11:  IL_0000: ldarg.0 
  12: IL_0001: call instance void 
  13: [mscorlib]System.Object::.ctor() 
  14:  IL_0006: ret 
  15: }

由此看来,和实例字段一样,如果一个类里有静态字段需要初始化,C#编译器会自动的生成一个静态的构造器,然后把这些初始化工作搬移到静态构造器里,所以代码1自动的产生了一个静态构造器是因为_count静态字段的初始化,因为静态构造器要确保在类型使用之前它的所有静态成员都已经初始化了。

从代码2生成的代码来看,如果你显式的给一个类定义了一个静态构造器(而不是编译器自动生成的),这好比就是你告诉编译器,不要生成那个beforefieldinit。

那这个东东到底什么作用呢?

首先,C#编译器会为所有没有提供显式静态构造器的类型生成这个beforefieldinit。

我们来看看Ecma-335如何描述beforefieldinit:

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.
If not marked BeforeFieldInit then that type’s initializer method is executed at (i.e., is triggered
by):
• first access to any static field of that type, or
• first invocation of any static method of that type or
• first invocation of any constructor for that type.

稍作翻译:

如果一个类型标记为beforefieldinit,则类型初始化器会在第一次访问该类型的任何静态字段时或之前任何时候执行。

如果没有标记beforefieldinit,则类型初始化器会在:

第一次访问该类型的任何静态字段或

第一次调用该类型的任何静态方法或

第一次调用该类型的任何构造器

从这里可以看出,标没标记beforefieldinit,实际上就是类型初始化器的执行时刻不同,如果标记了,则这个类型初始化器(就是静态构造器)只须在第一次访问这个类型的静态字段之前任何时刻执行就ok了,而没有标记就不同,这个时候类型初始化器的运行时刻非常确定。

这样一来有什么影响呢?

会对性能有些微妙的影响,请看下面实例:

//没有显式提供类型初始化器,C#编译器会为其生成beforefieldinit元数据

   1: public class Foo
   2: {
   3:  public static int Count = 5;
   4: } 


//显式提供了类型初始化器,C#编译器不会为其生成beforefieldinit元数据

   1: public class Foo
   2: {
   3:  public static int Count = 5;
   4:  static Foo(){}
   5: }

测试程序:

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:          Stopwatch stopWatcher = new Stopwatch();
   6:          stopWatcher.Start();
   7:          for(int i = 0;i < int.MaxValue-1;i++){
   8:             int count = Foo.Count;
   9:          }
  10:          stopWatcher.Stop();
  11:         Console.WriteLine(stopWatcher.ElapsedMilliseconds);
  12:     }
  13: }

当测试程序中的Foo类显式提供类型初始化器的时候比没有显式提供的慢十倍左右(在我的Dell D630上测试)。当然,如果for循环的次数比较少,这个差别还是看不出来的。

对于类型初始化器还有其他一些特征:

1.不会自动的去调用基类的类型初始化器。如果需要从上到下的调用一个继承链中的类型初始化器该怎么办?Ecma-335里给出了两种方法:

• defining hidden static fields and code in each class constructor that touches the hidden static field of its
base class and/or interfaces it implements, or
• by making explicit calls to System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor

利用显式静类型初始化器会在第一次访问静态字段时调用这个特性,在基类里定义一个隐藏的静态字段(不起任何其他作用),然后在子类的类型初始化器里访问基类的这个静态字段,这样基类的类型初始化器就会调用了。

第二种方法还可以通过System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor这个方法直接去调用类型初始化器。(System.Runtime.CompilerServices.RuntimeHelpers是一个挺有意思的类,可以关注一下)。