购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.5 继承

为了提高软件模块的可复用性和可扩充性,以便提高软件的开发效率,我们总是希望不仅能够利用前人或自己以前的开发成果,而且希望在自己的开发过程中能够有足够的灵活性,不拘泥于复用的模块。今天任何面向对象的程序设计语言都必须提供两个重要的特性:继承性(inheritance)和多态性(polymorphism)。

如果所有的类都处在同一级别上,这种没有相互关系的平坦结构就会限制系统面向对象的特性。继承的引入就是在类之间建立一种相交关系,使得新定义的派生类的实例可以继承已有的基类的特征和能力,而且可以加入新的特性或者修改已有的特性建立起类的层次。

同一操作作用于不同的对象可以有不同的解释,产生不同的执行结果,这就是多态性。多态性通过派生类重载基类中的虚函数型方法来实现。

2.5.1 C#的继承机制

2.5.1.1 概述

现实世界中的许多实体之间不是相互孤立的,它们往往具有共同的特征,也存在内在的差别,人们可以采用层次结构来描述这些实体之间的相似之处和不同之处。

为了用软件语言对现实世界中的层次结构进行模型化,面向对象的程序设计技术引入了继承的概念:一个类从另一个类派生出来时,派生类从基类那里继承特性,派生类也可以作为其他类的基类从一个基类派生出来。多层类形成了类的层次结构。注意:C#中派生类只能从一个类中继承,这是因为在C++中,人们在大多数情况下不需要一个从多个类中派生的类,从多个基类中派生一个类往往会带来许多问题,从而抵消了这种灵活性带来的优势。

C#中派生类从它的直接基类中继承成员、方法、域、属性、事件、索引指示器,除了构造函数和析构函数,派生类隐式地继承了直接基类的所有成员。

程序清单 2-27:

Vehicle作为基类,体现了汽车这个实体具有的公共性质:汽车都有车轮和重量。Car类继承了Vehicle的这些性质,并且添加了自身的特性:可以搭载乘客。C#中的继承符合下列规则:

1)继承是可传递的,如果C从B中派生,B又从A中派生,那么C不仅继承了B中声明的成员,同样也继承了A中的成员。Object类作为所有类的基类。

2)派生类应当是对基类的扩展,派生类可以添加新的成员但不能除去已经继承的成员的定义。

3)构造函数和析构函数不能被继承,除此以外的其他成员不论对它们定义了怎样的访问方式,都能被继承。基类中成员的访问方式只能决定派生类能否访问它们。

4)派生类如果定义了与继承而来的成员同名的新成员,就可以覆盖已继承的成员,但这并不意味着派生类删除了这些成员,只是不能再访问这些成员。

5)类可以定义虚方法、虚属性以及虚索引指示器,它的派生类能够重载这些成员,因此类可以展示出多态性。

2.5.1.2 覆盖

我们上面提到类的成员,声明中可以声明与继承而来的成员同名的成员,这时称派生类的成员覆盖了基类的成员,这种情况下编译器不会报告错误,但会给出一个警告。对派生类的成员使用new关键字,可以关闭这个警告。前面汽车类的例子中,类Car继承了Vehicle的Speak方法,可以给Car类也声明一个Speak方法,覆盖Vehicle中的Speak,见下面的代码:

程序清单 2-28:

注意:如果在成员声明中加上了new关键字修饰,而该成员事实上并没有覆盖继承的成员,编译器将会给出警告。在一个成员声明同时使用new和override,则编译器会报告错误。

2.5.1.3 base保留字

base关键字主要是为派生类调用基类成员提供一个简写的方法。我们先看一个例子,程序代码如下:

类B从类A中继承B的方法,G中调用了A的方法、F和索引指示器方法。F在进行编译时等价于

使用base关键字对基类成员的访问格式为

2.5.2 多态性

在面向对象的系统中,多态性是一个非常重要的概念,它允许客户对一个对象进行操作,由对象来完成一系列的动作,具体实现哪个动作、如何实现由系统负责解释。

2.5.2.1 C#中的多态性

多态性一词最早用于生物学,指同一种族的生物体具有相同的特性。在C#中多态性的定义是:同一操作作用于不同的类的实例,不同的类将进行不同的解释,最后产生不同的执行结果。C#支持两种类型的多态性:

1.编译时的多态性

编译时的多态性是通过重载来实现的,我们在2.3节中介绍了方法的重载和操作符重载,它们都实现了编译时的多态性。对于非虚的成员来说,系统在编译时根据传递的参数、返回的类型等信息决定实现何种操作。

2.运行时的多态性

运行时的多态性就是指直到系统运行时才根据实际情况决定实现何种操作,C#中运行时的多态性通过虚成员实现。编译时的多态性具有运行速度快的特点,而运行时的多态性则具有高度灵活和抽象的特点。

2.5.2.2 虚方法

类中的方法声明前加上virtual修饰符称为虚方法,反之称为非虚方法。使用了virtual修饰符后不允许再有static、abstract或override修饰符。对于非虚的方法,无论被其所在类的实例调用还是被这个类的派生类的实例调用,方法的执行方式不变;而对于虚方法,它的执行方式可以被派生类改变。这种改变是通过方法的重载来实现的。下面的例子说明了虚方法与非虚方法的区别。

程序清单 2-29:

例子中类A提供了两个方法:非虚方法F和虚方法G。类B则提供了一个新的非虚方法F,从而覆盖了继承的类F,B同时还重载了继承的方法G,那么输出应该是

A.F

B.F

B.G

B.G

注意到本例中方法a.G()实际调用了B.G而不是A.G,这是因为编译时值为A但运行时值为B,所以B完成了对方法的实际调用。

2.5.2.3 在派生类中对虚方法进行重载

先让我们回顾一下普通的方法重载。普通的方法重载指的是类中两个以上的方法(包括隐藏的继承而来的方法)取的名字相同,只要使用的参数类型或者参数个数不同,编译器便知道在何种情况下应该调用哪个方法。而对基类虚方法的重载是函数重载的另一种特殊形式,在派生类中重新定义此虚函数时,要求方法名称、返回值类型、参数表中的参数个数、类型顺序都必须与基类中的虚函数完全一致,在派生类中声明对虚方法的重载要求在声明中加上override关键字,而且不能有new、static或virtual修饰符。

还是用汽车类的例子来说明多态性的实现:

分析上面的例子看到:

1)Vehicle类中的Speak方法被声明为虚方法,那么在派生类中就可以重新定义此方法。

2)在派生类Car和Truck中,分别重载了Speak方法,派生类中的方法原型和基类中的方法原型必须完全一致。

3)在Test类中创建了Vehicle类的实例v1,并且先后指向Car类的实例c1和Truck类的实例t1。

运行该程序结果应该是

这里Vehicle类的实例v1先后被赋予Car类的实例c1以及Truck类的实例t1的值,在执行过程中v1先后指代不同的类的实例,从而调用不同的版本。这里v1的Speak方法实现了多态性,并且v1.Speak究竟执行哪个版本不是在程序编译时确定的,而是在程序的动态运行时根据v1某一时刻的指代类型来确定的,所以还体现了动态的多态性。

2.5.3 抽象与密封

2.5.3.1 抽象类

有时候基类并不与具体的事物相联系,而是只表达一种抽象的概念,用以为它的派生类提供一个公共的界面,为此C#中引入了抽象类(abstract class)的概念。注意C++程序员在这里最容易犯错误,C++中没有对抽象类进行直接声明的方法,而认为只要在类中定义了纯虚函数,这个类就是一个抽象类。纯虚函数的概念比较晦涩,直观上不容易为人们接受和掌握,因此C#抛弃了这一概念。

抽象类使用abstract修饰符,对抽象类的使用有以下几点规定:

1)抽象类只能作为其他类的基类,它不能直接被实例化,而且对抽象类不能使用new操作符。抽象类如果含有抽象的变量或值,则它们要么是null类型,要么包含了对非抽象类的实例的引用。

2)抽象类允许包含抽象成员,虽然这不是必需的。

3)抽象类不能同时又是密封的。

如果一个非抽象类从抽象类中派生,则其必须通过重载来实现所有继承而来的抽象成员,请看下面的示例:

抽象类A提供了一个抽象方法F,类B从抽象类A中继承并且又提供了一个方法G,因为B中并没有包含对F的实现,所以B也必须是抽象类,类C从类B中继承并重载了抽象方法F,并且提供了对F的具体实现,则类C允许是非抽象的。

让我们继续研究汽车类的例子。我们从交通工具这个角度来理解Vehicle类的话,它应该表达一种抽象的概念。我们可以把它定义为抽象类:由轿车类Car和卡车类Truck来继承这个抽象类,它们作为可以实例化的类。

程序清单 2-30:

2.5.3.2 抽象方法

由于抽象类本身表达的是抽象的概念,因此类中的许多方法并不一定要有具体的实现,而只是留出一个接口来作为派生类重载的界面。举一个简单的例子:图形这个类是抽象的,它的成员方法计算图形面积也就没有实际的意义,面积只对图形的派生类,比如圆、三角形这些非抽象的概念才有效,那么就可以把基类图形的成员方法计算面积声明为抽象的,具体的实现交给派生类通过重载来实现。一个方法声明中如果加上abstract修饰符,称该方法为抽象方法(abstract method)。

如果一个方法被声明是抽象的,那么该方法默认也是一个虚方法。事实上抽象方法是一个新的虚方法,它不提供具体的方法实现代码。我们知道非虚的派生类要求通过重载为继承的虚方法提供自己的实现,而抽象方法则不包含具体的实现内容,所以方法声明的执行体中只有一个分号,只能在抽象类中声明抽象方法,对抽象方法不能再使用static或virtual修饰符,而且方法不能有任何可执行代码,哪怕只是一对大括号中间加一个分号都不允许出现,只需要给出方法的原型就可以了。

交通工具的鸣笛这个方法实际上是没有什么意义的,接下来利用抽象方法的概念继续改写汽车类的例子:

程序清单 2-31:

还要注意抽象方法在派生类中不能使用base关键字来进行访问,例如下面的代码在编译时会发生错误:

还可以利用抽象方法来重载基类的虚方法,这时基类中虚方法的执行代码被拦截了,下面的例子说明了这一点:

类A声明了一个虚方法F,派生类B使用抽象方法重载了F,这样B的派生类C就可以重载F并提供自己的实现。

2.5.3.3 密封类

想想看,如果所有的类都可以被继承,继承的滥用会带来什么后果?类的层次结构体系将变得十分庞大,类之间的关系杂乱无章,对类的理解和使用都会变得十分困难。有时我们并不希望自己编写的类被继承,另一些时候有的类已经没有再被继承的必要。C#提出了一个密封类(sealed class)的概念帮助开发人员来解决这一问题。

密封类在声明中使用sealed修饰符,这样就可以防止该类被其他类继承。如果试图将一个密封类作为其他类的基类,C#将提示出错。密封类不能同时又是抽象类,因为抽象类总是希望被继承的。在哪些场合下使用密封类呢?密封类可以阻止其他程序员在无意中继承该类,而且密封类可以起到运行时优化的效果,实际上密封类中不可能有派生类,如果密封类实例中存在虚成员函数,该成员函数可以转化为非虚的函数,修饰符virtual不再生效。

让我们看下面的例子:

如果我们尝试写下面的代码:

C#会指出这个错误,告诉你B是一个密封类,不能试图从B中派生任何类。

2.5.3.4 密封方法

我们已经知道使用密封类可以防止对类的继承,C#还提出了密封方法(sealed method)的概念,以防止在方法所在类的派生类中对该方法的重载。对方法可以使用sealed修饰符,这时称该方法是一个密封方法。不是类的每个成员方法都可以作为密封方法,密封方法必须对基类的虚方法进行重载提供具体的实现方法,所以在方法的声明中,sealed修饰符总是和override修饰符同时使用,请看下面的例子代码:

类B对基类A中的两个虚方法均进行了重载,其中F方法使用了sealed修饰符,成为一个密封方法,G方法不是密封方法,所以在B的派生类C中可以重载方法G,但不能重载方法F。

2.5.4 继承中关于属性的一些问题

和类的成员方法一样,也可以定义属性的重载、虚属性、抽象属性以及密封属性的概念。

与类和方法一样,属性的修饰也应符合下列规则:

1.属性的重载

1)在派生类中使用修饰符的属性表示对基类中的同名属性进行重载。

2)在重载的声明中,属性的名称类型访问修饰符都应该与基类中被继承的属性一致。

3)如果基类的属性只有一个属性访问器,重载后的属性也应只有一个,但如果基类的属性同时包含get和set属性访问器,重载后的属性可以只有一个也可以同时有两个属性访问器。

注意:与方法重载不同的是,属性的重载声明实际上并没有声明新的属性,而只是为已有的虚属性提供访问器的具体实现。

2.虚属性

1)使用virtual修饰符声明的属性为虚属性。

2)虚属性的访问器包括get访问器和set访问器,同样也是虚的抽象属性。

3)使用abstract修饰符声明的属性为抽象属性。

4)抽象属性的访问器也是虚的,而且没有提供访问器的具体实现,这就要求在非虚的派生类中由派生类自己通过重载属性来提供对访问器的具体实现。

5)同时使用abstract和override修饰符不但表示属性是抽象的,而且它重载了基类中的虚属性,这时属性的访问器也是抽象的。

6)抽象属性只允许在抽象类中声明。

7)除了同时使用abstract和override修饰符这种情况之外,static、virtual、override和abstract修饰符中任意两个不能再同时出现密封属性。

8)使用sealed修饰符声明的属性为密封属性,类的密封属性不允许在派生类中被继承,密封属性的访问器同样也是密封的。

9)属性声明时如果有sealed修饰符,同时也必须要有override修饰符。

从上面可以看出:属性的这些规则与方法十分类似,对于属性的访问器可以把get访问器看成是一个与属性修饰符相同,没有参数返回值为属性的值类型的方法把set访问器看成是一个与属性修饰符相同仅含有一个value参数返回类型为void的方法。下面这个例子可以说明属性在继承中的一些问题:

程序清单 2-32:

上面的例子中声明了人这个类,人的姓名Name和性别Sex是两个只读的虚属性,身份证号Card是一个抽象属性,允许读写。因为类People中包含了抽象属性Card,所以People必须声明是抽象的。下面为住宿的客人编写一个类,类从People中继承。

程序清单 2-33:

在类Customer中,属性Name、Sex和Card的声明都加上了override修饰符,属性的声明都与基类People中保持一致,Name和Sex的get访问器、Card的get和set访问器都使用了base关键字来访问基类People中的访问器,属性Card的声明重载了基类People中的抽象访问器,这样在Customer类中没有抽象成员的存在,Customer可以是非虚的。 BgrhNBrgoHrFN7w415ZcCnTUJMI/Ur8+n540FQlbW1n5iXqxl/oK3wdQ9k0OlHQe

点击中间区域
呼出菜单
上一章
目录
下一章
×