.net你必须知道的事儿 1.2

什么是继承

继承,一个熟悉而容易产生误解的话题,这是大部分人最直观的感受。说它熟悉,是因为作为面向对象的三大要素之一的继承,每个技术研究者都会在职业生涯中不断的重复关于继承的话题;说他容易产生误解,是应为他总是和封装,多态交织在一起,形成复杂的局面,以继承为例如何理清多层继承的机制,如何了解实现继承与接口继承的异同,如何体会继承与多态的关系,似乎都不是件简单的事情。

本节希望将继承中最为头疼,最为复杂的问题统统拿出来晒一晒,以防时间久了,不知不觉在使用者那里发霉生虫。

1.2.2 基础为上

正如引言所述,继承是个容易产生误解的技术话题,那么,对于继承,就应该着手从这些容易误解与引起争论的话题来寻找光宇全面认识和了解继承的答案,一点一滴摆出来,最后再对分析的要点做归纳,形成一种系统化的认识,这是一种探索问题的方式,用于剖析继承这一话题真是在恰当不过啦

不过解密之前我们还是按照技术分析的惯例,从基本出发,以简洁的方式来快速了解关于继承最基本的概念,首先,认识一张比较简单的动物分类图,以便引入我们对继承概念的介绍。

 从图1-1中,我们可以获得的信息包括:

动物继承关系是以一定的分类规则进行的,将相同属性和特征的动物及其类别抽象为一类,类别与类别之间的关系反映为对相似或者对不相似的某种抽象关系,列如鸟类一般都能飞,而鱼类一般都生活在水中。

位于继承图下层的类别继承了上层所有类别的特性,形成一种IS-A的关系,列如我们可以说,人类IS-A哺乳类,人类IS-a脊椎类。但是这种关系是单向的,所以我们不能说鸟类IS-A鸡。

动物继承图自上而下是一种逐层具体化的过程,而自下而上是一种逐层抽象化的过程,这种抽象化关系反映为上下层之间的继承关系,列如,最高层的动物具有最普遍的特征,而最底层的人则具有较为具体的特征。

下层类型只能从上层类型中的某一个类别继承,例如鲸类的上层只能是一种哺乳动物,因此是一种单继承形式

这种继承关系中,层与层的特性是向下传递的,列如鸟类具有脊椎类的特征,鹤类也具有脊柱类的特征,而所有的类都具有动物的特征,因此说动物是这个层次关系的根。

我们将这种现实世界的对象抽象化,就形成了面向对象世界的继承机制,因此,关于继承,我们可以定义为:

继承,就是面向对象中类与类之间的一种关系。继承的类称为子类,派生类,而被继承类称为父类,基类或超类,通过继承,使得子类具有父类的属性和方法,同时子类也可以通过加入新的属性和方法修改父类的属性和方法建立新的类层次。

继承机制体现了面向对象技术中的复用性,扩展性和安全性,为面向对象软件开发与模块化软件架构提供了最基本的技术基础。

在.net中继承按照其实现方式的不同,一般分类如下

实现继承:派生类继承了基类的所有属性和方法,并且只能有一个基类 在.net中system.object是所有类型的最终基类,这种继承方式称为实现继承。

接口继承:派生类继承了接口的方法签名,不同于实现继承的是,接口继承允许多继承,同时派生类只继承了方法签名而没有方法实现,具体的实现必须在派生类中完成。因此,确切的说。这种继承方式应该称为接口实现。

CLR支持实现单继承和接口多继承,本节重点关注对象的实现继承,关于接口继承,我们将在1.5节"玩转接口"中做详细论述。另外,值得关注的是继承的可见性问题,.net通过访问权限来实现不同的控制规则,这些访问修饰符主要包括:public protected internal private.

下面我们就以动物继承情况为例,实现一个简单的继承实例,如图1-2所示

 在这个继承体系中。我们实现了一个简单的三层继承层次,animal类是所有类型的基类,在此将其构造为抽象类,抽象了所有的类型的普遍特征行为,Eat方法和ShowType方法,其中showType方法为虚函数,其具体实现在子类Chicken和Eagle中给出,这种在子类中实现虚函数的方式,称为方法的动态绑定,是实现面向对象另一特性:多态的基本机制。另外,Eagle类实现了接口继承,使得Eagle实例可以实现Fly这一特性,接口继承的优点是显而易见的,通过IFlyable接口,实现了对象与行为的分离,这样我们无须担心因为继承不当而使Chicken有Fly的能力,保护了系统的完整性。

从图1-2所示的UML图中可知,通过继承我们轻而易举地实现了代码的复用和扩展,同时通过重载(overload)。覆写(override),接口实现等方式实现了封装变化,隐藏私有信息等面向对象的基本规则。通过继承,轻易得实现了子类对父类共性的继承,列如,animal类中实现了方法Eat(),那么它的所有子类就都具有了Eat()特性,同时,子类也可以实现对基类的扩展和改写,主要有两种方式,一是通过在子类中添加新方法,列如Brid类中就添加了新方法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 abstract 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内存分配为止。我们可以在编译期中用单步执行的方法来大致了解其分配的过程和顺序,因此,对象的创建过程是按照顺序完成了对整个父类及其本身字段的内存创建,并且字段的存储循序是由上到下排列,最高层类的字段排在最前面,其原因是如果父类和子类出现了同名字段,则在子类对象创建时,编译期会自动认为这是两个不同的字段而加以区别。

1.继承是可以传递的,子类是对父类的扩展,必须继承父类方法,同时可以添加新方法

2.子类可以调用父类方法和字段,而父类不能调用子类方法和字段。

3.虚方法如何实现覆写操作,使得父类指针可以指向子类对象成员。

4.子类不光继承父类的公有成员,同时继承了父类的私有成员,只是在子类中不被访问。

5.new关键字在虚方法继承中的阻断作用。

你是否已经找到了理解继承,理解动态编程的不二法门?

通过上面的讲述与分析,我们基本上对.net在编译器的实现原理有了大致的了解,但是还有以下的问题,可能会引起疑惑,那就是:

Bird bird=new Chicken();

这种情况下,bird2.showType应该返回什么值呢?而bird2.type又该是什么值呢?有两个原则,是.net专门用于解决这一问题的

关注对象原则:调用子类还是父类的方法,取决于创建的对象是子类还是父类对象,而不是它的引用类型。列如Bird bird2=new Chicken()时,我们关注的是其创建对象为Chicken类型,因此子类将继承父类的字段和方法,或者覆写父类的虚方法,而不用关注bird2的引用类型是否为Bird。引用类型的区别决定了不同的对象在方法表中不同的访问权限

根据关注对象原则,下面的两种情况又该如何区别呢?

Bird bird2=new Chicken();

Chicken chicken=new Chicken();

根据上文的分析,bird2对象和chicken对象在内存布局上是一样的,差别就在于其引用指针的类型不同;bird2为Bird类型指针,而chicken为Chicken类型指针,以方法调用为例,不同的类型指针在虚拟方法表中有不同的附加信息作为标志来区别其访问的地址区域,称为offset。不同类型的指针只能在其特定地址区域内执行,子类覆盖父类时会保证其访问地址区域的一致性,从而解决了不同的类型访问具有不同的访问权限问题

执行就近原则:对于同名字段或者方法,编译器是按照其顺序查找来引用的,也就是首先访问离他创建最近的字段或者方法

1.2.4秘境追踪

通过对继承的基本内容的讨论和本质揭示,是时候将我们的眼光转移到继承应用中的热点问题了,主要是从面向对象的角度对继承进行讨论,就像追踪继承中的秘境,在迷失的森林中寻找入口

1.实现继承与接口继承

实现继承通常情况下表现为对抽象类的继承,而其与接口继承在规则上以下几点归纳;

抽象类适合于有族层概念的类间关系,而接口最适合1为不同的类提供通用功能。

接口着重于can-do关系类型,而抽象类则偏重于is-a式地关系。

接口多定义对象的行为,抽象类多定义对象的属性。

如果预计会出现版本问题,可以创建“抽象类”。列如,创建了狗,鸡和鸭那么应该考虑抽象出动物来应对以后可能出现马和牛的事情,而向接口中添加新成员则会强制要求修改所有派生类,并重新编译,所以版本式的问题最好以抽象类来实现

因为值类型是密封的所以只能实现接口,而不能继承类。

关于实现继承与接口继承的更详细的讨论与规则,请参见8.4节"面向抽象编程:接口和抽象类"。

2.聚合还是继承,这是个问题

类与类的关系,通常有一下几种情况,我们分别以两个简单类Class1和Class2的UML图来表示如下。

1.继承

如图1-4所示,class2继承自class1,如何对基类class1的更改都有可能影响到子类class2,继承关系的耦合度较高。

2.聚合

如图1-5所示

 聚合分为三种类型,依次为无,共享和符合,其耦合度逐级递增。无聚合类型关系,类的双方彼此不受影响;共享型关系,class2不需要对class1负责,而符合型关系,class1会受控于class2的更改,因此耦合度更高。总之聚合关系是一种has-a式的关系,耦合度没有继承关系高。

3.依赖

依赖关系表明,如果class被修改则class1会受到影响,如图1-6所示

 通过上述三类关系的比较,我们知道类与类之间的关系,通常耦合度来描述,也就是表示类与类之间的依赖关系程度。没有耦合关系的系统是根本不存在的,因为类与类,模块与模块,系统与系统之间或多或少要发生相互交互,设计应力求将类与类之间的耦合关系降到最低,而面向对象的基本原则之一就是实现低耦合,高内聚的耦合关系,在2.1节"OO原则综述"中所述的合成/聚合复用原则正是这一思想德集中体现。

显然,将耦合德概念应用到继承机制上,通常情况下子类都会对父类产生紧密的耦合,对基类德修改往往会对子类产生一系列的不良反应,继承之毒瘤主要体现在:

继承可能造成子类的无限膨胀,不利于类体系的维护和安全。

继承的子类对象确定于编译期,无法满足需要运行期才能确定的情况,而聚合类很好的解决了这一问题。

随着继承层次的复杂化和子类的多样化,不可避免地会出现对父类的无效继承或者有害继承,子类部分的继承父类的方法或者属性,更能适应实际的设计需求。

那么,通过上面的分析,我们深知继承机制在满足更加柔性的需求方面有一些弊端,从而可能造成系统设计漏洞与失衡,解决问题的办法当然是多种多样的,根据不同的需求进行不同的设计变更,列入将对象与行为分离抽象出接口实现来避免大基类设计,以聚合代替继承实现更柔性的子类需求等。

面向对象的基本原则

多聚合,少继承。

低耦合,高内聚

聚合与继承通常体现在设计模式的伟大思想中,在此以Adapter模式的两种方式为例来比较继承和聚合的适应场合与柔性较量,首先对Adapter模式进行简单的介绍,Adapter模式主要用于将一个类的接口转换为另外一个接口,通常情况下在不改变原有体系的条件下应对新的需求变化,通过引入新的适配器来完成对既存体系的扩展和改造,adpater模式就其实现方式主要包括;

类的adpater模式,通过引入新的类型来继承原有类型,同时实现新加入的接口方法,其缺点是耦合度搞,需要引入过多的新类型。

对象的adapter模式,通过聚合而非继承的方式来实现对原有系统的扩展,松散耦合,较少的新类型。

下面,我们回到动物体系中,为鸟儿加上鸣叫totweet这一行为,为自然界点缀更多美丽的声音,当然不同的鸟儿叫声是不同的,鸡鸣鹰斯,各有各的范儿,因此在bird类的子类都因该对tosweet有不同的实现。现在我们的要求是在不破坏原有设计的基础上来为bird实现itweetable接口,理所当然,以adapater模式来实现这一需求,通过类的Adapter模式和对象的adapter模式两种方式来感受其差别。

首先是类的adpater模式,其设计uml图表示为图1-7

在这一新设计体系中,两个新类型ChickenAdapter和EagleAdapter就是类的Adapter模式中新添加的类,

他们分别继承自原有的类,从而保留原有类型特性与行为,并实现添加ITweetable接口的新行为ToTweet().我们没有破坏原有的Bird体系,同时添加了新的行为,这是继承的魔力在Adapter模式中的应用。我们在客户端应用新的类型来为Chicken调用新的方法,如图1-8所示,原有继承体系中的方法和新的方法对对像ca都是可见的

我们轻松的完成了这一课题,是否该轻松一下?不事实上还早着了,要知道自然界里的鸟儿梦都有美丽的歌喉,我们只为Chicken和eagle配上了鸣叫的行为,那其他成千上万的鸟儿梦都有意见了,怎么办呢?以目前的实现方式我们不得不为每个继承子Bird类的子类提供相应的适配类,这样太累啦,有没有更好的方式呢?

答案是淡然有,这就是对象的Adapter模式,类的Adapter模式以继承方式来实现,而对象的Adaopter模式则以聚合的方式来完成详情如图1-9所示。

具体的实现细节为

interface ITweetable{
  void ToTweet();  
}
public class BirdAdapter:ITweetable{
   private Bird _bird;
   public BirdAdapter(BIrd bird){
     _bird=bird;
   }  
    public void ToTweet(){
       //为不同的子类实现不同的ToTweet行为
    }
}
public class TestInheritance{
   public static void Main(){
     BirdAdapter ba=new BirdAdapter(new chicken());
     ba.showtype();
     ba。tosweet();
   }
}

现在可以松口气啦。我们以聚合的方式按照对象的adapter模式思路来解决为Bird类及其子类加入tosweet()行为的操作,在没有添加过多新类型的基础上十分轻松的解决了这一问题,看起来一切都很完美,新的BirdAdapter类与Bird类型之间只有松散的耦合关系而不是紧耦合。

至此,我们以一个几乎完整的动物系设计基本完成了对继承与聚合问题的探讨,系统设计是一个复杂,兼顾,重构的过程,不管是继承还是聚合,都是系统设计过程中必不可少的技术基础,采取什么样的方式来实现完全取决于具体的需求情况,根据面向对象组合,少继承的原则,对象的adapter模式更能体现松散的耦合关系,应用更灵活。

1.2.5规则制胜

根据本节的所有讨论,行文至此,我们很有必要对继承进行归纳总结,将继承概念中的中的重点内容和重点规则做系统的梳理,对我们来说这些规则条款是掌握继承的金科玉律,主要包括;

密封类不可以被继承。

继承关系中,我们更多的是关注其共性而不是特性,因为共性是层次复用的基础,而特性是系统扩展的基点。

实现单继承,接口多继承。从宏观来看,继承多关注于共通性,而多态多着眼于差异性。

继承的层次应该有所控制,否则类型之间的关系维护会消耗跟多的精力

面向对象原则,多组合,少继承,低耦合,高内聚。

1.2.6

在,net中如果创建一个类,则该类总是在继承,这缘于,net的面向对象特性,所有的类型都最终继承自共同的根System.object。可见,继承是。net运行机制的基础技术之一,一切结尾对象,一切基于继承对于什么是继承这个话题,也希望本舰试着一旅程的起点。

原文地址:https://www.cnblogs.com/555556J/p/13410382.html