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


    一个类型最多只能有一个基类,但是可以实现多个接口,接口不是基类。接口不能有基类,但是可以有多个父接口。没有实现任何接口,也没有基类的类型实际上隐式的从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关键字修饰这个类。

  • 相关阅读:
    一个表对应另一个表中多个主键的查询方法(把一个表当成两个表用)
    可以切换数据库的SqlHelper
    win7安装后的用户选择
    如何删除 Windows.old 文件夹
    Windows Server 2008磁盘清理工具
    sqlserver express版PRIMARY 大小不能超过4G
    一交换机,一光猫、一路由器组internet网的方法
    公司部门职责清晰
    IIS下载EXE(拾遗)
    win2008 IIS 7.0中WebDAV
  • 原文地址:https://www.cnblogs.com/yuyijq/p/1532884.html
Copyright © 2020-2023  润新知