第4章继承第3章介绍了如何使用C#中的各个类,其重点是如何定义方法、构造函数、属性和单个类(或单个结构)中的其他成员。我们指出,所有的类最终都派生于System.Object类,但并没有说明如何创建继承类的层次结构。继承是本章的主题。我们将简要讨论C#对继承的支持,然后详细论述如何在C#中编码实现(implementation)继承和接口继承。注意,本章假定您已经熟悉了继承的基本概念,包括虚函数和重写。我们将重点阐述用于提供继承的语法和与继承相关的主题,例如虚函数,C#继承模型的其他方面是C#所特有的,其他面向对象的语言都不具备。4.1继承的类型首先介绍C#在继承方面支持和不支持的功能。4.1.1实现继承和接口继承面向对象编程的开发人员知道,有两种截然不同的继承类型:实现继承和接口继承。●实现继承:表示一个类型派生于一个基类型,拥有该基类型的所有成员字段和函数。在实现的继承中。派生类型的每个函数采用基类型的实现,除非在派生类型的定义中指定重写该函数的实现。在需要给现有的类型添加功能,或许多相关的类型共享一组重要的公共功能时,这种类型的继承是非常有效的。例如第19章讨论的WindowsForms类(第19章也讨论了基类System.Windows.Forms.Control,该类提供了常用Windows控件的非常复杂的实现,第19章还讨论了许多其他的类,例如System.Windows.Forms.TextBox和System.Windows.Forms.ListBox,这两个类派生于Control,并重写了函数,或提供了新的函数,以实现特定类型的控件)。●接口继承:表示一个类型只继承了函数的签名,没有继承任何实现。在需要指定该类型具有某些可用的特性时,最好使用这种类型的继承。例如,某些类型可以指定从接口System.IDisposable(详见第7章)中派生,从而提供一种清理资源的方法Dispose()。由于某种类型清理资源的方式可能与另一种类型的完全不同,所以定义通用的实现是没有意义的,此时就适合使用接口继承。接口继承常常被看做提供了一种契约:通过类型派生于接口,从而保证为客户提供某个功能。在传统上,像C++这样的语言在实现的继承方面功能非常强大,实际上,实现继承是C++编程模型的核心。另一方面,VB6不支持类的任何实现继承,但因其底层的COM基础体系,所以它支持接口继承。在C#中,既有实现继承,也有接口继承。它们没有强弱之分,因为这两种继承都完全内置于语言中,因此很容易为不同的情形选择最好的体系结构。第4章继承•95•4.1.2多重继承一些语言如C++支持所谓的“多重继承”,即一个类派生于多个类。使用多重继承的优点是有争议的:一方面,毫无疑问,可以使用多重继承编写非常复杂、但很紧凑的代码,如C++ALT库。另一方面,使用多重实现继承的代码常常很难理解和调试(这也可以从C++ALT库中看出)。如前所述,使健壮代码的编写容易一些,是开发C#的重要设计目标。因此,C#不支持多重实现继承。而C#又允许类型派生于多个接口。这说明,C#类可以派生于另一个类和任意多个接口。因为System.Object是一个公共的基类,所以每个C#类(除了Object类之外)都有一个基类,还可以有任意多个基接口。4.1.3结构和类第3章区分了结构(值类型)和类(引用类型)。使用结构的一个限制是结构不支持继承,但每个结构都自动派生于System.ValueType。实际上还应更仔细一些:不能建立结构的类型层次,但结构可以实现接口。换言之,结构并不支持实现继承,但支持接口继承。事实上,定义结构和类可以总结为:●结构总是派生于System.ValueType,它们还可以派生于任意多个接口。●类总是派生于用户选择的另一个类,它们还可以派生于任意多个接口。4.2实现的继承如果要声明一个类派生于另一个类,可以使用下面的语法:classMyDerivedClass:MyBaseClass{//functionsanddatamembershere}注意:这个语法非常类似于C++和Java中的语法,但是,C++程序员习惯于使用公共和私有继承的概念,要注意C#不支持私有继承,因此基类名上没有public或private限定符。支持私有继承会大大增加语言的复杂性,实际上私有继承在C++中也很少使用。如果类(或结构)也派生于接口,则用逗号分隔开基类和接口:publicclassMyDerivedClass:MyBaseClass,IInterface1,IInterface2{//etc.对于结构,语法如下:publicstructMyDerivedStruct:IInterface1,IInterface2{//etc.C#高级编程(第3版)•96•如果在类定义中没有指定基类,C#编译器就假定System.Object是基类。因此下面的两段代码生成相同的结果:classMyClass:Object//derivesfromSystem.Object{//etc.}和classMyClass//derivesfromSystem.Object{//etc.}为了简单一些,第二种形式比较常用。C#支持object关键字,它用作System.Object类的假名,所以也可以编写下面的代码:classMyClass:object//derivesfromSystem.Object{//etc.}如果要引用Object类,可以使用object关键字,智能编辑器(如VS.NET)会识别它,并对代码进行合适的编辑。4.2.1虚方法把一个基类函数声明为virtual,该函数就可以在任何派生类中重写了:classMyBaseClass{publicvirtualstringVirtualMethod(){returnThismethodisvirtualanddefinedinMyBaseClass;}}也可以把属性声明为virtual。对于虚属性或重写属性,语法与非虚属性是相同的,但要在定义中加上关键字virtual,其语法如下所示:publicvirtualstringForeName{get{returnforeName;}set{foreName=value;}}privatestringforeName;第4章继承•97•为了简单起见,下面的讨论将主要集中于方法,但其规则也适用于属性。C#中虚函数的概念与标准OOP概念相同:可以在派生类中重写虚函数。在调用方法时,会调用对象类型的合适方法。在C#中,函数在默认情况下不是虚拟的,但(除了构造函数以外)可以显式地声明为虚拟。这遵循C++的方式,即从性能的角度来看,除非显式指定,否则函数就不是虚拟的,而在Java中,所有的函数都是虚拟的。但C#的语法与C++的语法不同,因为C#要求在派生类的函数重写另一个函数时,要使用override关键字显式声明:classMyDerivedClass:MyBaseClass{publicoverridestringVirtualMethod(){returnThismethodisanoverridedefinedinMyDerivedClass;}}方法重写的语法避免了C++中很容易发生的潜在运行错误:当派生类的方法签名无意中与基类版本略有差别时,派生类方法就不能重写基类方法了。在C#中,这会出现一个编译错误,因为编译器会认为函数已标记为override,但没有重写它的基类方法。成员字段和静态函数都不能被声明为virtual,因为这个概念只对类中的实例函数成员有意义。4.2.2隐藏方法如果签名相同的方法在基类和派生类中都进行了声明,但该方法没有声明为virtual和override,派生类方法就会隐藏基类方法。结果是调用哪个类的方法取决于用于引用实例的变量类型,而不是实例本身的类型。在大多数情况下,是要重写方法,而不是隐藏方法,因为隐藏方法会存在为给定类的实例调用错误方法的危险。但是,C#语法可以确保开发人员在编译时收到这个潜在错误的警告,使隐藏方法更加安全。这也是类库开发人员得到的版本方面的好处。假定有人编写了类HisBaseClass:classHisBaseClass{//variousmembers}在将来的某一刻,要编写一个自己的派生类,给HisBaseClass添加某个功能,特别是要添加一个目前基类中没有的方法MyGroovyMethod():classMyDerivedClass:HisBaseClass{publicintMyGroovyMethod(){//somegroovyimplementationC#高级编程(第3版)•98•return0;}}一年后,基类的编写者决定扩展基类的功能。为了保持一致,他也添加了一个名为MyGroovyMethod()的方法,该方法的名称和签名与前面添加的方法的名称和签名相同,但并不完成相同的工作。在使用基类的新方法编译代码时,在应该调用哪个方法上就会有潜在的冲突。这在C#中完全合法,但因为我们的MyGroovyMethod()与基类的MyGroovyMethod()不相关,运行这段代码的结果就可能不是我们希望的结果。C#已经为此设计了一种方式,可以很好地处理这种情况。首先,系统会发出警告。在C#中,应使用new关键字声明我们要隐藏一个方法,如下所示:classMyDerivedClass:HisBaseClass{publicnewintMyGroovyMethod(){//somegroovyimplementationreturn0;}}但是,我们的MyGroovyMethod()没有声明为new,所以编译器会认为它隐藏了基类的方法,但没有显式声明,因此发出一个警告(这也适用于把MyGroovyMethod()声明为virtual)。如果愿意,可以给我们的方法重命名,以响应该警告。这么做,是最好的情形,因为这会避免许多冲突。但是,如果觉得重命名方法是不可能的(例如,已经为其他公司把软件发布为一个库,所以无法修改方法的名称),则所有的已有客户机代码仍能正确运行,选择我们的MyGroovyMethod()。这是因为访问这个方法的已有代码必须通过对MyDerivedClass(或进一步派生的类)的引用进行选择。已有的代码不能通过对HisBaseClass的引用访问这个方法,因为在对HisBaseClass的早期版本进行编译时,会产生一个编译错误。这个问题只会发生在将来编写的客户机代码上。C#会发出一个警告,告诉用户在将来的代码中可能会出问题——用户应注意这个警告,不要试图在将来的代码中通过对HisBaseClass的引用调用MyGroovyMethod()方法,但所有已有的代码仍会正常工作。这是比较微妙的,但很好地说明了C#如何处理类的不同版本。4.2.3调用函数的基础版本C#有一种特殊的语法用于从派生类中调用方法的基础版本:base.MethodName()。例如,假定派生类中的一个方法要返回基类的方法返回的值的90%,就可以使用下面的语法:classCustomerAccount{publicvirtualdecimalCalculatePrice(){第4章继承•99•//implementationreturn0.0M;}}classGoldAccount:CustomerAccount{publicoverridedecimalCalculatePrice(){returnbase.CalculatePrice()*0.9M;}}这个语法类似于Java,但Java使用关键字super而不是base。C++没有类似的关键字,但需要对类名进行显式说明(CustomerAccount:CalculatePrice())。C++中对应于base的内容都比较模糊,因此C++允许多重继承。注意,可以使用base.MethodName()语法调用基类中的任何方法,不必是在同一个方法的重载中调用它。4.2.4抽象类和抽象函