代码改变世界

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

2009-07-28 12:29  横刀天笑  阅读(1015)  评论(0编辑  收藏  举报

一个类型最多只能有一个基类,但是可以实现多个接口,接口不是基类。接口不能有基类,但是可以有多个父接口。没有实现任何接口,也没有基类的类型实际上隐式的从System.Object继承。

一个非常有趣的地方是,从不同的基类继承在CLR里还有不同的语义。比如值类型从System.ValueType继承,而所有可封送的对象从System.MarshalByRefObject继承,还有一个System.ContextBoundObject,嗯,还有委托。

在上一章讨论静态的相关东西时,已经说过,如果你使用abstract关键字修饰一个类型,那么这个类就不能被实例化,那这样的类天生就是作基类的命,如果你不想让一个类作为基类,那就用sealed修饰它吧。嗯,这样一下sealed和abstract两个冤家倒是不能碰在一起,不然一个天生就是做基类,一个有不允许它做基类,这不就要PK了么,也许你已经看了上一篇文章,其实这两个冤家在IL里还真碰到了一起,那就是为了实现2.0引入的static类型,不过注意,在C#层面这两个是不能放在一起的。

基类里面的非私有的成员,会隐式的作为派生类的成员(告诉都不告诉你一声,就自动的跑到派生类了)。那如果基类与派生类都定义了一个同名的字段,那在基类里不出现两个同名的字段了么,那该怎么访问呢?那就要先看看这个字段是静态的还是实例的,如果是静态字段,好说,我们只需要用类型名引用就OK了:

   1: public class Base
   2: {
   3:     public static int _field;
   4: }
   5: public class Child : Base
   6: {
   7:     public static int _field;
   8:     public void Test()
   9:     {
  10:         Base._field = 5;
  11:         Child._field = 6;
  12:     }
  13: }

那要是该字段是实例字段该怎么办,嘿嘿,C#已经为我们准备了this和base关键字:

   1: public class Base
   2: {
   3:     public int _field;
   4: }
   5: public class Child : Base
   6: {
   7:     public int _field;
   8:     public void Test()
   9:     {
  10:         //这里默认是加了一个this关键字
  11:         _field = 5;
  12:         //实际上和上一句的意义是一样的
  13:         this._field = 5;
  14:          //通过base关键字访问基类里的成员
  15:         base._field = 7;
  16:     }
  17: }

上面是从派生类内部访问字段,那如果是在外部访问呢:

   1: Child c = new Child();
   2: Base b = c;
   3: c._field = 5;
   4: b._field = 6;

那么c._field访问的和b._field访问有什么区别呢,它们其实引用的都是同一个对象啊,不过由于b和c两个变量的类型不一样,所以它们看到的契约也不同。通过c访问_field的时候,由于Child隐藏了Base类里面的_field,所以这里毫无疑问,访问的是Child类里面的_field,如果用b变量访问呢,由于b是Base类型的,它并不知道Child的契约(或者公有的接口,就是一些公有成员),所以它访问的是Base类里的_field。实际上,我们使用ILDasm看看内部IL代码:

   1: //给Child的_field赋值
   2:   IL_000a:  ldc.i4.5
   3:   IL_000b:  stfld      int32 BaseType.Child::_field
   4:  
   5: //给Base的_field赋值
   6:   IL_0011:  ldc.i4.6
   7:   IL_0012:  stfld      int32 BaseType.Base::_field

毫无疑问,这里的访问在编译期间就已经确定了。也就是所谓的静态绑定。

在编译上面的程序的时候,实际上我们还发现编译器生成了一个警告:

'BaseType.Child._field' hides inherited member 'BaseType.Base._field'. Use the new keyword if hiding was intended.

这个警告其实可以忽略,意思就是让你在Child上加个关键字new,标识这个_field字段隐藏了基类Base里的同名字段,实际上你加不加new对编译器最后生成的代码、元数据没有任何的影响,影响的是编译器的表现行为:编译器不再给警告了。C#里的这个new关键字负的责任太多了,实际上这里用一个new关键字并不恰当,还是VB.NET更加亲切,VB.NET对于这种情况使用Shadows关键字。

上面说的都是字段,那对于方法呢?方法跟字段可不同,字段就一个名字、一个类型,方法还有参数列表呢。因为要实现方法的重载,所以处理方法名的重用与字段名的重用有些不同。

CLR对于基类和派生类有相同名的方法时有两种策略:hide-by-signature和hide-by-name。这是通过是否在方法的元数据里添加hidebysig元数据实现的。顾名思义,hide-by-signature就是,不仅方法名相同,要签名也相同,派生类才能隐藏基类里的同签名的方法,够狠。那如果用hide-by-name的话,派生类里只要有一个Test方法,那基类里的所有Test方法,不管是有没有参数,多少个参数,都会被派生类里的那个Test方法给隐藏了。

对于这个策略是编译器相关的,对于C#编译器,默认就是hide-by-signature,而对于VB.NET编译器你可以使用Overloads(hide-by-signature)与Shadows(hide-by-name)关键字,对于C++,默认就是hide-by-name,这是由于“古典”C++的遗留问题决定的。好了,我们来看个示例吧:

   1: public class Base
   2: {
   3:     public void Test()
   4:     {}
   5:     public void Test(object o)
   6:     {}
   7: }
   8: public class Child : Base
   9: {
  10:     public new void Test()
  11:     {}
  12:     public void Test(int i)
  13:     {}
  14: }

由于这是使用的C#,所以默认的是hide-by-signature。

   1: Child c = new Child();
   2: Base b = c;
   3: //调用的是Child类里的Test(),因为它隐藏了基类里的Test()
   4: c.Test();
   5: //调用的是Base里的Test()
   6: b.Test();
   7: //调用的是Child里的Test(int)
   8: c.Test(5);
   9: //调用的是Base的Test(object)
  10: c.Test(“hello”);

实际上,你可以使用ILDasm看看,这里的调用关系在编译时已经确定了,不涉及到任何运行时绑定,CLR还对运行对方法的动态绑定提供支持,这个会在本书后面的相关内容里讨论。

嗯,本章还剩下最后一个问题,在一个继承树中,构造函数是如何调用的?

看下面一段程序(这段程序直接录自.NET本质论):

   1: public class Base
   2:     {
   3:         public int x = a();
   4:         public Base()
   5:         {
   6:             b();
   7:         }
   8:  
   9:         static int a()
  10:         {
  11:             return 2;
  12:         }
  13:         private void b() { }
  14:  
  15:     }
  16:     public class D1 : Base
  17:     {
  18:         public int y = c();
  19:         public D1()
  20:         {
  21:             d();
  22:         }
  23:         static int c()
  24:         {
  25:             return 3;
  26:         }
  27:         private void d() { }
  28:     }
  29:  
  30:     public class D2 : D1
  31:     {
  32:         public int z = e();
  33:         public D2()
  34:         {
  35:             f();
  36:         }
  37:         static int e()
  38:         {
  39:             return 4;
  40:         }
  41:         private void f() { }
  42:     }
  43:     public class D3 : D2
  44:     {
  45:         public int w = g();
  46:         public D3()
  47:         {
  48:             h();
  49:         }
  50:         static int g()
  51:         {
  52:             return 5;
  53:         }
  54:         private void h() { }
  55: }

当调用D3的构造函数,实例化的时候到底发生了什么事情呢?

我们用ILDasm反编译出D3的构造器的IL代码:

   1: IL_0001:  call       int32 BaseType.D3::g()
   2:   IL_0006:  stfld      int32 BaseType.D3::w
   3:   IL_000b:  ldarg.0
   4:   IL_000c:  call       instance void BaseType.D2::.ctor()
   5: IL_0013:  ldarg.0
   6:   IL_0014:  call       instance void BaseType.D3::h()

我们发现,编译器将public int w = g()这段代码插入到了构造器的第一行,然后又调用D3的基类D2的构造器,然后再到D3构造器本来就有的h方法,实际上,上面的D2,D1,Base的调用规则都是如此,所以简简单单一个实例化,却实际上发生了一连串的事情:

D3.ctor->g()->D2.ctor->e()->D1.ctor->c()->Base.ctor->a()->Object.ctor->b()->d()->f()->h()

最佳实践

由于继承等涉及的东西太多,如果你设计一个类,暂时还不想让它派生新类,那么就应该用sealed关键字修饰它,直到有必要要从它派生新类的时候才去掉。而且,如果一个类,确定只在程序集内部使用,那么就请使用internal关键字修饰这个类。