1.2什么是继承本节将介绍以下内容:—什么是继承?—继承的实现本质—继承的分类与规则—继承与聚合—继承的局限1.2.1引言继承,一个熟悉而容易产生误解的话题。这是大部分人对继承最直观的感受。说它熟悉,是因为作为面向对象的三大要素之一的继承,每个技术研究者都会在职业生涯中不断地重复关于继承的话题;说它容易产生误解,是因为它总是和封装、多态交织在一起,形成复杂的局面。以继承为例,如何理清多层继承的机制,如何了解实现继承与接口继承的异同,如何体会继承与多态的关系,似乎都不是件简单的事情。本节希望将继承中最为头疼,最为复杂的问题统统拿出来晒一晒,以防时间久了,不知不觉在使用者那里发霉生虫。本节不会花太多笔墨做系统性的论述,如有需要请参考其他技术专著上更详细的分析。我们将从关于继承的热点出发,逐个击破,最后总结规律,期望用这种方式实现对继承全面的了解,让你掌握什么才是继承。1.2.2基础为上正如引言所述,继承是个容易产生误解的技术话题。那么,对于继承,就应该着手从这些容易误解与引起争论的话题来寻找关于全面认识和了解继承的答案。一点一滴摆出来,最后再对分析的要点做归纳,形成一种系统化认识。这是一种探索问题的方式,用于剖析继承这一话题真是再恰当不过了。不过,解密之前,我们还是按照技术分析的惯例,从基本出发,以简洁的方式来快速了解关于继承最基本的概念。首先,认识一张比较简单的动物分类图(图1-1),以便引入我们对继承概念的介绍。图1-1继承关系图从图1-1中,我们可以获得的信息包括:—动物继承关系是以一定的分类规则进行的,将相同属性和特征的动物及其类别抽象为一类,类别与类别之间的关系反映为对相似或者对不相似的某种抽象关系,例如鸟类一般都能飞,而鱼类一般都生活在水中。—位于继承图下层的类别继承了上层所有类别的特性,形成一种IS-A的关系,例如我们可以说,人类IS-A哺乳类、人类IS-A脊椎类。但是这种关系是单向的,所以我们不能说鸟类IS-A鸡。—动物继承图自上而下是一种逐层具体化过程,而自下而上是一种逐层抽象化过程,这种抽象化关系反映为上下层之间的继承关系。例如,最高层的动物具有最普遍的特征,而最低层的人则具有较具体的特征。—下层类型只能从上层类型中的某一个类别继承,例如鲸类的上层只能是哺乳类一种,因此是一种单继承形式。—这种继承关系中,层与层的特性是向下传递的,例如鸟类具有脊椎类的特征,鹤类也具有脊椎类的特征,而所有的类都具有动物的特征,因此说动物是这个层次关系的根。我们将这种现实世界的对象抽象化,就形成了面向对象世界的继承机制。因此,关于继承,我们可以定义为:继承,就是面向对象中类与类之间的一种关系。继承的类称为子类、派生类,而被继承类称为父类、基类或超类。通过继承,使得子类具有父类的属性和方法,同时子类也可以通过加入新的属性和方法或者修改父类的属性和方法建立新的类层次。继承机制体现了面向对象技术中的复用性、扩展性和安全性。为面向对象软件开发与模块化软件架构提供了最基本的技术基础。在.NET中,继承按照其实现方式的不同,一般分类如下。—实现继承:派生类继承了基类的所有属性和方法,并且只能有一个基类,在.NET中System.Object是所有类型的最终基类,这种继承方式称为实现继承。—接口继承:派生类继承了接口的方法签名。不同于实现继承的是,接口继承允许多继承,同时派生类只继承了方法签名而没有方法实现,具体的实现必须在派生类中完成。因此,确切地说,这种继承方式应该称为接口实现。CLR支持实现单继承和接口多继承。本节重点关注对象的实现继承,关于接口继承,我们将在1.5节“玩转接口”中做详细论述。另外,值得关注的是继承的可见性问题,.NET通过访问权限来实现不同的控制规则,这些访问修饰符主要包括:public、protected、internal和private。下面,我们就以动物继承情况为例,实现一个最简单的继承实例,如图1-2所示。图1-2动物系统UML在这个继承体系中,我们实现了一个简单的三层继承层次,Animal类是所有类型的基类,在此将其构造为抽象类,抽象了所有类型的普遍特征行为:Eat方法和ShowType方法,其中ShowType方法为虚函数,其具体实现在子类Chicken和Eagle中给出。这种在子类中实现虚函数的方式,称为方法的动态绑定,是实现面向对象另一特性:多态的基本机制。另外,Eagle类实现了接口继承,使得Eagle实例可以实现Fly这一特性,接口继承的优点是显而易见的:通过IFlyable接口,实现了对象与行为的分离,这样我们无需担心因为继承不当而使Chicken有Fly的能力,保护了系统的完整性。从图1-2所示的UML图中可知,通过继承我们轻而易举地实现了代码的复用和扩展,同时通过重载(overload)、覆写(override)、接口实现等方式实现了封装变化,隐藏私有信息等面向对象的基本规则。通过继承,轻易地实现了子类对父类共性的继承,例如,Animal类中实现了方法Eat(),那么它的所有子类就都具有了Eat()特性。同时,子类也可以实现对基类的扩展和改写,主要有两种方式:一是通过在子类中添加新方法,例如Bird类中就添加了新方法ShowColor用于现实鸟类的毛色;二是通过对父类方法的重新改写,在.NET中称为覆写,例如Eagle类中的ShowColor()方法。1.2.3继承本质论了解了关于继承的基本概念,我们回归本质,从编译器运行的角度来揭示.NET继承中的运行本源,来发现子类对象如何实现对父类成员与方法的继承,以简单的示例揭示继承的实质,来阐述继承机制是如何被执行的。publicabstractclassAnimal{publicabstractvoidShowType();publicvoidEat(){Console.WriteLine(Animalalwayseat.);}}publicclassBird:Animal{privatestringtype=Bird;publicoverridevoidShowType(){Console.WriteLine(Typeis{0},type);}privatestringcolor;publicstringColor{get{returncolor;}set{color=value;}}}publicclassChicken:Bird{privatestringtype=Chicken;publicoverridevoidShowType(){Console.WriteLine(Typeis{0},type);}publicvoidShowColor(){Console.WriteLine(Coloris{0},Color);}}然后,在测试类中创建各个类对象,由于Animal为抽象类,我们只创建Bird对象和Chicken对象。publicclassTestInheritance{publicstaticvoidMain(){Birdbird=newBird();Chickenchicken=newChicken();}}下面我们从编译角度对这一简单的继承示例进行深入分析,从而了解.NET内部是如何实现我们强调的继承机制的。(1)我们简要地分析一下对象的创建过程:Birdbird=newBird();Birdbird创建的是一个Bird类型的引用,而newBird()完成的是创建Bird对象,分配内存空间和初始化操作,然后将这个对象引用赋给bird变量,也就是建立bird变量与Bird对象的关联。(2)我们从继承的角度来分析CLR在运行时如何执行对象的创建过程,因为继承的本质正体现于对象的创建过程中。在此我们以Chicken对象的创建为例,首先是字段,对象一经创建,会首先找到其父类Bird,并为其字段分配存储空间,而Bird也会继续找到其父类Animal,为其分配存储空间,依次类推直到递归结束,也就是完成System.Object内存分配为止。我们可以在编译器中用单步执行的方法来大致了解其分配的过程和顺序,因此,对象的创建过程是按照顺序完成了对整个父类及其本身字段的内存创建,并且字段的存储顺序是由上到下排列,最高层类的字段排在最前面。其原因是如果父类和子类出现了同名字段,则在子类对象创建时,编译器会自动认为这是两个不同的字段而加以区别。然后,是方法表的创建,必须明确的一点是方法表的创建是类第一次加载到AppDomain时完成的,在对象创建时只是将其附加成员TypeHandle指向方法列表在LoaderHeap上的地址,将对象与其动态方法列表相关联起来,因此方法表是先于对象而存在的。类似于字段的创建过程,方法表的创建也是父类在先子类在后,原因是显而易见的,类Chicken生成方法列表时,首先将Bird的所有方法复制一份,然后和Chicken本身的方法列表做对比,如果有覆写的虚方法则以子类方法覆盖同名的父类方法,同时添加子类的新方法,从而创建完成Chicken的方法列表。这种创建过程也是逐层递归到Object类,并且方法列表中也是按照顺序排列的,父类在前子类在后,其原因和字段大同小异,留待读者自己体味。不言而喻,任何类型方法表中,开始的4个方法总是继承自System.Object类型的虚方法,它们是:ToString、Equals、GetHashCode和Finalize,详见8.1节“万物归宗:System.Object”所述。结合我们的分析过程,现在将对象创建的过程以图例来揭示其在内存中的分配情形,如图1-3所示。图1-3对象创建内存概括从我们的分析和上面的对象创建过程中,我们应对继承的本质有了以下更明确的认识:—继承是可传递的,子类是对父类的扩展,必须继承父类方法,同时可以添加新方法。—子类可以调用父类方法和字段,而父类不能调用子类方法和字段。—虚方法如何实现覆写操作,使得父类指针可以指向子类对象成员。—子类不光继承父类的公有成员,同时继承了父类的私有成员,只是在子类中不被访问。—new关键字在虚方法继承中的阻断作用。你是否已经找到了理解继承、理解动态编译的不二法门?通过上面的讲述与分析,我们基本上对.NET在编译期的实现原理有了大致的了解,但是还有以下的问题,可能会引起疑惑,那就是:Birdbird2=newChicken();这种情况下,bird2.ShowType应该返回什么值呢?而bird2.type又该是什么值呢?有两个原则,是.NET专门用于解决这一问题的。—关注对象原则:调用子类还是父类的方法,取决于创建的对象是子类对象还是父类对象,而不是它的引用类型。例如Birdbird2=newChicken()时,我们关注的是其创建对象为Chicken类型,因此子类将继承父类的字段和方法,或者覆写父类的虚方法,而不用关注bird2的引用类型是否为Bird。引用类型的区别决定了不同的对象在方法表中不同的访问权限。注意根据关注对象原则,下面的两种情况又该如何区别呢?Birdbird2=newChicken();Chickenchicken=newChicken();根据上文的分析,bird2对象和chicken对象在内存布局上是一样的,差别就在于其引用指针的类型不同:bird2为Bird类型指针,而chicken为Chicken类型指针。以方法调用为例,不同的类型指针在虚拟方法表中有不同的附加信息作为标志来区别其访问的地址区域,称为offset。不同类型的指针只能在其特定地址区域内执行,子类覆盖父类时会保证其访问地址区域的一致性,从而解决了不同的类型访问具有不同的访问权限问题。—执行就近原则:对于同名字段或者方法,编译器是按照其顺序查找来引用的,也就是首先访问离它创建最近的字段或者方法,例如上例中的bird2,是Bird类型,因此会首先访问Bird_type(注意编译器是不会重新命名的,在此是为区分起见),如果type类型设为public,则在此将返回“Bird”值。这也就是为什么在对象创建时必须将字段按顺序排列,而父类要先于子类