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

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。