第14章 继 承
- 继承的引入是在类别之间建立一种交叉关系,使新定义的衍生实例能够继承现有基本类别的特征和能力,并添加新的特征或修改现有的特征来建立类别的水平。
- 多态 —— 在不同的对象中使用相同的操作,可以有不同的解释,并产生不同的执行结果。通过衍生重载基类中的虚拟函数类方法实现多态性。
14.1 C#继承机制
14.1.1 概述
[外链图片存储失败,源站可能有防盗链机制,建议保存图片直接上传(img-2nws08N9-1647832731680)(1401.jpg)] 最高层的实体往往具有最常见、最常见的特征。下层的东西越具体,下层包含上层的特征。它们之间的关系是基本类和衍生类之间的关系。 —— 为了用软件语言模型现实世界的层次结构,面向对象的程序设计技术引入了继承的概念。一个类是从另一个类中衍生出来的,衍生类是从基类中继承出来的。也可以作为其他类别的基类。从一个基类中衍生出来的多层类形成了一个类的层次结构。
- c# 中派生只能从一个类中继承。派生类从其基类中继承成员: 方法、域、属性、事件、索引指示器。 派生类除了构造函数和分析函数外,还隐式继承了直接基类的所有成员。
程序清单 14-1
using System; using System.Collections.Generic; using System.Linq; using System.Text; /* vehicle 作为基类,体现了实体汽车的公共性质;汽车有车轮和重量。Car类继承了vehicle 的性质 并添加了自己的特点——可搭载乘客。 */ namespace _14_1 { class vehicle //定义汽车 { public int wheels; // 公共成员:轮数 —— 如果少了 public ? protected float weight;// 保护成员:重量 public vehicle() {; } public vehicle (int w,float g) { wheels = w; weight = g; } public void Speak() { Console.WriteLine("the w vehicle is speaking!"); } } class Car:vehicle //定义轿车类:从汽车类中继承 { int passenger; ///私有成员:乘客数量:乘客数量: public Car(int w, float g, int p) : base(w, g) // { wheels = w; weight = g; passenger = p; } } }
- C# 继承符合下列规定:
- 继承是可以传递的。如果C从B中派生,B再次从A中衍生,那么C不仅继承了B中声明的成员,还继承了A中的成员。Object 作为所有类别的基类。
- 派生应该是基类的扩展。派生可以添加新成员,但不能删除继承成员的定义。
- 结构函数和分析函数不能继承。此外,其他成员可以继承,无论他们定义了什么样的访问方式。基本成员的访问只能决定衍生物是否可以访问。
- 如果派生定义了与继承成员同名的新成员,则可以覆盖继承成员。但这并不是因为派生删除了这些成员,而是因为他们不能访问它们。
- 类可以定义虚拟方法、虚拟属性和虚拟索引指示器,其衍生类可以重载这些成员,从而实现类可以显示多态性。
14.1.2 覆盖
正如我们上面提到的,同名成员可以在类成员的声明中声明。这时,我们称派生成员覆盖(hide)基本成员。在这种情况下,编译器不会报告错误,但会给出警告。使用派生成员new 这个警告可以关闭关键字。 程序清单 14-2:
using System; using System.Collections.Generic; using System.Linq; using System.Text; /* 派生类的成员覆盖hide 在这种情况下,编译器不会报告基本成员的错误 但是会给派生成员一个警告new 关键字可以关闭这个警告 前面汽车类的例子中类Car 继承了Vehicle 的Speak 我们可以给出方法Car 类也声明了一个Speak 方法覆盖Vehicle 中的Speak */ namespace _14_2 { internal class Vehicle { public int wheels; //公有成员:轮子个数 protected float weight; //保护成员:重量 public Vehicle() {; } public Vehicle(int w,float g) { wheels = w; weight = g; } public void Speak() { Console.WriteLine("the w vehicle is speaking!"); } } class Car : Vehicle //定义汽车类 { int passengers; ///私有成员:乘客数量:乘客数量:乘客数量:乘客数量 public Car(int w, float g, int p) : base(w, g) { wheels = w; weight = g; passengers = p; } new public void Speak() { Console.WriteLine("Di-di!"); } } /* 如果添加到成员声明中new 关键字修改,成员实际上没有覆盖 成员编译器将在成员声明中同时发出警告new 和 override 则编译器 会报告错误 */ }
14.1.3 base 保留字
base 关键字主要是为派生类调用基类成员提供一个简写的方法。
class A { public void F() { // F 具体执行代码 } public int this[int nIndex] { get{}; set{}; } } class B { public void G() { int x = base[0]; base.F(); } }
类B 继承A类,BA方法F和索引指示器被调用到方法G中。方法F在编译时等于: public void G() { int x = (A this )[0]; (A this ).F(); } 使用base 基类成员的关键字访问格式是 base . identifier base [ expression-list ]
14.2 多 态 性
多态性是一个非常重要的概念,它允许客户操作一个对象,有一个对象来完成一系列的动作,以及如何实现一个系统来解释它。
14.2.1 C#多态性。
-
c# 多态性的定义是—— 对于不同类别的实例,不同类别会进行不同的解释,最终产生不同的执行结果。C#支持两种类型的多态性:
编译时的多态性,—— 根据重载实现。 运行时的多态性,—— 是指根据实际情况决定实现什么操作,直到系统运行。通过虚拟成员实现。
编译过程中的多态性为我们提供了快速运行的特点,而多态性带来了高度灵活和抽象的特点。
14.2.2 虚方法
当类中的方法声明前加上了 Virtual 修饰符,我们称之为虚法,反之亦然。Virtual 修改后,不允许再有static 、abstract、override 修饰符。 对于非虚拟方法,该方法的执行方法保持不变,无论是被其类别的例子调用,还是被这类衍生类别的例子调用。对于虚拟方法,其执行方法可以通过方法的重载来改变。
程序清单 14-3:
using Syste; using System.Collections.Generic; using System.Linq; using System.Text; namespace _14_3 { class A { /// <summary> /// 非虚方法 F /// </summary> public void F() { Console.WriteLine("A.F"); } /// <summary> /// 虚方法 G /// </summary> public virtual void G() { Console.WriteLine("A.G"); } } class B:A { /// <summary> /// 类B 提供了一个新的非虚的方法F, /// 从而覆盖了继承的F; /// </summary> new public void F() { Console.WriteLine("B.F"); } /// <summary> /// 类B 同时还重载了继承的方法G。 /// </summary> public override void G() { Console.WriteLine("B.G"); } } }
/* 对于非虚的方法无论被其所在类的实例调用还是被这个类的派生类的实例调
用方法的执行方式不变而对于虚方法它的执行方式可以被派生类改变这种改 变是通过方法的重载来实现的 */
internal class Program { static void Main(string[] args) { B b = new B(); A a = b; a.F(); b.F(); a.G(); //方法a.G() 实际调用了B.G,而不是A.G。这是因为编译时值为A,但运行时值为B, //所有B完成了对方法的实际调用。 b.G(); } }
14.2.3 在派生类中对虚方法进行重载
普通的方法重载指的是:类中两个以上的方法,取的名字相同,只要使用的参数类型或者参数个数不同,编译器便知道在何种情况下应该调用那个方法。 —— 对基类虚方法的重载是函数重载的另种特殊形式。在派生类中重新定义此虚函数时,要求的是方法名称、返回值类型、参数表中的参数个数、类型、顺序都必须与基类中的虚函数完全一致。在派生类中声明对虚方法的重载,要求在声明中加上override 关键在,而且不能有new,static 或 virtual 修饰符。
程序清单 14-4:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace _14_4 { public class Vehicle { public int wheels; // 公有成员: 轮子 protected float weight; //保护成员: 重量 public Vehicle(int w, float g) { wheels = w; weight = g; } /// <summary> /// 声明虚方法,那么在派生类中就可以重新定义此方法。 /// </summary> public virtual void Speak() { Console.WriteLine("the w vehicle is speaking!"); } } public class Car : Vehicle //定义轿车类 { int passenger; //私有成员: 乘客数 public Car(int w, float g, int p) : base(w, g) { wheels = w; weight = g; passenger = p; } /// <summary> /// 在派生类Car 和Truck中分别重载了Speak方法, /// 派生类中的方法原型和基类中的方法原型必须完全一致。 /// </summary> public override void Speak() { Console.WriteLine("The car is speaking :Di-di!"); } } class Truck : Vehicle //定义卡车类 { int passengers; //私有成员:乘客数 float load; //私有成员:载重量 public Truck(int w, float g, int p, float l) : base(w, g) { wheels = w; weight = g; passengers = p; load = l; } public override void Speak() { Console.WriteLine("The truck is speaking :Ba-ba!"); } } } internal class Program { /// <summary> /// 创建了Vehicle类的实例v1, /// 并且先后Car类的实例 C1和Truck 类的实例t1. /// </summary> /// <param name="args"></param> static void Main(string[] args) { Vehicle v1 = new Vehicle(1,5); Car c1 = new Car(4, 2,5); Truck t1 = new Truck(6,5,3,10); v1.Speak(); v1 = c1; v1.Speak(); c1.Speak(); v1 = t1; v1.Speak(); t1.Speak(); /* Vehicle 类的实例v1 先后被赋予Car 类的实例c1 ,以及Truck 类的实例t1 的值。在执行过程中, v1 的 Speak 方法实现了多态性,并且v1.Speak() 究竟执行那个版本,不是在陈谷编译时确定的,而是 在程序的动态运行时,根据v1某一时刻的指代类型来确定,所有还体现了动态的多态性。 */ } } }
14.3 抽象与密封
14.3.1 抽象类
有时候,基类并不与具体的事物相联系,而是指表达一种抽象的概念,用以为它的派生类提供一个公共的界面。 (abstract class)的概念。
-
抽象类使用abstract修饰符,对抽象类的使用以下几点规定: 抽象类只能作为其他类的基类,他不能直接被实例化,而且对抽象类不能使用 new 操作符。抽象类如果含有抽象的变量或值,则它们要么是null 类型,要么包含了对非抽象类的实例的引用。
-
抽象类允许包含抽象成员,虽然这不是必须的。
-
抽象类不能同时又是密封的。 如果一个非抽象类从抽象类中派生,则其必须通过重载来实现所有继承而来的抽象成员。
abstract class A { public abstract void F(); } abstract class B: A { public void G() {} } class C: B { public override void F() { // F 的具体实现代码 } }
抽象类A 提供了一个抽象方法F 类B 从抽象类A 中继承并且又提供了一个方 法G 因为B 中并没有包含对F 的实现所以B 也必须是抽象类类C 从类B 中继承 类中重载了抽象方法F 并且提供了对F 的具体实现则类C 允许是非抽象的 让我们继续研究汽车类的例子我们从交通工具这个角度来理解Vehicle 类的 话它应该表达一种抽象的概念我们可以把它定义为抽象类由轿车类Car 和卡车 类Truck 来继承这个抽象类它们作为可以实例化的类
-
程序清单 14-5
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
/*
我们从交通工具这个角度来理解Vehicle 类的
话它应该表达一种抽象的概念我们可以把它定义为抽象类由轿车类Car 和卡车
类Truck 来继承这个抽象类它们作为可以实例化的类
*/
namespace _14_5
{
internal class vehicle //定义汽车类
{
public int wheels; //公有成员:轮子个数
protected float weight; //保护成员:重量
public vehicle(int w,float g)
{
wheels = w;
weight = g;
}
public virtual void Speak()
{
Console.WriteLine("the w vehicle is spaeking!");
}
}
class Car : vehicle //定义轿车类
{
int passengers; //私有成员:乘客数
public Car(int w, float g, int p) : base(w, g)
{
wheels = w;
weight = g;
passengers = p;
}
public override void Speak()
{
Console.WriteLine("The car is speaking:Di-di!");
}
}
class Truck:vehicle //定义卡车
{
int passengers; //私有成员:乘客数
float load; // 私有成员:载重量
public Truck(int w,float g,int p,float l):base(w,g)
{
wheels = w;
weight = g;
passengers = p;
load = l;
}
public override void Speak()
{
Console.WriteLine("The truck is speaking:Ba-ba!");
}
}
}
14.3.2 抽象方法
由于抽象类本身表达的是抽象的概念,因此类中的许多方法并不一定要有具体的实现,而只是留出一个接口来作为派生类重载的界面。举一个简单的例子,“图形”这个类是抽象的,它的成员方法“计算图形面积”也就没有实际的意义。面积只对“图形”的派生类不如“圆”,“三角形”这些非抽象的概念才有效,那么我们就可以把基类“图形”的成员方法“计算面积”声明为抽象的,具体的实现交给派生类通过重载来实现。
一个方法声明中如果加上 abstract 修饰符,我们称该方法为抽象方法(abstractmethod)。
如果 一个方法被声明也是抽象的,那么该方法默认也是一个虚方法。事实上,抽象方法是一个新的虚方法,它不提供具体的方法实现代码。我们知道,非虚的派生类要求通过重载为继承的虚方法提供自己的实现,而抽象方法则不包含具体的实现内容,所有方法声明的执行体中只有一个分号";"
只能在抽象类中声明抽象方法。对抽象方法,不能再使用static 或virtual 修饰符,而且方法不能有任何可执行代码,哪怕只是一对大括号中间加一个一个分号{;}
都不允许出现,只需要给出方法的原型就可以了。
程序清单 14-6:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace _14_6
{
abstract class vehicle //定义汽车类
{
public int wheels; //公有成员;轮子个数
protected float weight; //保护成员;重量
public vehicle(int w,float g)
{
wheels = w;
weight = g;
}
public abstract void Speak()
;
}
class Car:vehicle //定义轿车类
{
int passengers; // 私有成员;乘客数
public Car(int w,float g,int p):base(w,g)
{
wheels = w;
weight = g;
passengers = p;
}
public override void Speak()
{
Console.WriteLine("The car is speaking : Di-di!");
}
}
class Truck:vehicle //定义卡车类
{
int passengers; //私有成员:乘客数
float load; //私有成员;载重量
public Truck(int w,float g,int p,float l):base(w,g)
{
wheels = w;
weight = g;
passengers = p;
load = l;
}
public override void Speak()
{
Console.WriteLine("The truck is speaking:Ba-ba!");
}
}
}
还要注意,抽象方法在派生类中不能使用 base 关键字来进行访问。
class A
{
public abstract void F();
}
class B: A
{
public override void F()
{
base.F(); // 错误 base.F 是抽象方法
}
}
我们还可以利用抽象方法来重载基类的虚方法这时基类中虚方法的执行代码就 被拦截了下面的例子说明了这一点
class A
{
public virtual void F()
{
Console.WriteLine("A.F");
}
}
abstract class B: A
{
public abstract override void F();
}
class C: B
{
public override void F()
{
Console.WriteLine("C.F");
}
}
类A 声明了一个虚方法F 派生类B 使用抽象方法重载了F 这样B 的派生类C 就可以重载F 并提供自己的实现
14.3.3 密封类
想想看如果所有的类都可以被继承继承的滥用会带来什么后果类的层次结 构体系将变得十分庞大类之间的关系杂乱无章对类的理解和使用都会变得十分困 难有时候我们并不希望自己编写的类被继承另一些时候有的类已经没有再被 继承的必要C#提出了一个密封类sealed class 的概念帮助开发人员来解决这一 问题 密封类在声明中使用sealed 修饰符这样就可以防止该类被其它类继承如果试 图将一个密封类作为其它类的基类C#将提示出错理所当然密封类不能同时又是 抽象类因为抽象总是希望被继承的 在哪些场合下使用密封类呢密封类可以阻止其它程序员在无意中继承该类而 且密封类可以起到运行时优化的效果实际上密封类中不可能有派生类如果密封 类实例中存在虚成员函数该成员函数可以转化为非虚的函数修饰符virtual 不再生 效 让我们看下面的例子
abstract class A
{
public abstract void F();
}
sealed class B: A
{
public override void F()
{
// F 的具体实现代码
}
}
如果我们尝试写下面的代码 class C: B{ } C#会指出这个错误告诉你B 是一个密封类不能试图从B 中派生任何类
14.3.4 密封方法
使用密封类可以防止对类的继承C#还提出了密封方法sealed method 的概念以防止在方法所在类的派生类中对该方法的重载 对方法可以使用sealed 修饰符这时我们称该方法是一个密封方法 不是类的每个成员方法都可以作为密封方法密封方法必须对基类的虚方法进行 重载提供具体的实现方法所以在方法的声明中sealed 修饰符总是和 override 修饰符同时使用。 程序清单 14-7:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace _14_7
{
internal class A
{
public virtual void F()
{
Console.WriteLine("A.F");
}
public virtual void G()
{
Console.WriteLine("A.G");
}
}
class B:A
{
public sealed override void F()
{
Console.WriteLine("B.F");
}
public override void G()
{
Console.WriteLine("B.G");
}
}
class C:B
{
public override void G()
{
Console.WriteLine("C.G");
}
}
/*
类B 对基类 A 中的两个虚方法均进行了重载,其中 F 方法使用了 sealed 修饰符,
成为一个密封方法。G 方法不是密封方法,所以在 B 的派生类C中,可以重载方法G,但
不能重载方法F。
*/
}
14.4 继承中关于属性的一些问题
和类的成员方法一样我们也可以定义属性的重载虚属性抽象属性以及密封 属性的概念 与类和方法一样属性的修饰也应符合下列规则 属性的重载 在派生类中使用修饰符的属性表示对基类中的同名属性进行重载 在重载的声明中属性的名称类型访问修饰符都应该与基类中被继承的 属性一致 如果基类的属性只有一个属性访问器重载后的属性也应只有一个但如果 基类的属性同时包含get 和set 属性访问器重载后的属性可以只有一个也可以同时 有两个属性访问器 注意与方法重载不同的是属性的重载声明实际上并没有声明新的属性而只 是为已有的虚属性提供访问器的具体实现 虚属性 使用 virtual 修饰符声明的属性为虚属性 虚属性的访问器包括get 访问器和set 访问器同样也是虚的 抽象属性 使用 abstract 修饰符声明的属性为抽象属性 抽象属性的访问器也是虚的而且没有提供访问器的具体实现这就要求在 非虚的派生类中由派生类自己通过重载属性来提供对访问器的具体实现
abstract 和override 修饰符的同时使用不但表示属性是抽象的而且它重载 了基类中的虚属性这时属性的访问器也是抽象的 抽象属性只允许在抽象类中声明 除了同时使用abstract 和override 修饰符这种情况之外static, virtual, override 和abstract 修饰符中任意两个不能再同时出现 密封属性 使用 sealed 修饰符声明的属性为密封属性类的密封属性不允许在派生类中被 继承密封属性的访问器同样也是密封的 属性声明时如果有sealed 修饰符同时也必须要有override 修饰符 从上面可以看出属性的这些规则与方法十分类似对于属性的访问器我们可 以把get 访问器看成是一个与属性修饰符相同没有参数返回值为属性的值类型的方 法把set 访问器看成是一个与属性修饰符相同仅含有一个value 参数返回类型为 void 的方法还记得第十章中客户住宿的例子吗还是让我们扩展这个例子来说明属 性在继承中的一些问题
程序清单 14-8
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace _14_8
{
public enum sex
{
woman,
man,
};
abstract public class People
{
private string s_name;
public virtual string Name
{
get { return s_name; }
}
}
private sex m_sex;
public virtual sex Sex{
get{ return m_sex; }
}
protected string s_card;
public abstract string Card
{
get;set;
}
}
程序清单 14-9
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace _14_9
{
class Custome: People
{
string s_no;
int i_day;
public string No
{
get { return s_no; }
set
{
if (s_no != value)
{
s_no = value;
}
}
}
public int Day
{
get { return i_day; }
set
{
if (i_day != value)
{
i_day = value;
}
}
}
public override string Name
{
get { return base.Name; }
}
public override sex Sex
{
get { return base.Sex; }
}
public override string Card
{
get { return s_card; }
set { s_card = value; }
}
}
}
在类Customer 中属性Name Sex 和Card 的声明都加上了override 修饰符属 性的声明都与基类People 中保持一致Name 和Sex 的get 访问器Card 的get 和set 访问器都使用了base 关键字来访问基类People 中的访问器属性Card 的声明重载了 基类People 中的抽象访问器这样在Customer 类中没有抽象成员的存在Customer 可以是非虚的
14.5 小 结
继承是面向对象系统中一个非常重要的概念C#语言为我们提供了一整套设计良好的继承机制包括:
- 派生类对基类的继承
- 方法的继承
- 属性及其访问器的继承 在C#中还提供了抽象和密封的概念给继承方式带来了高度的灵活性大大方便 了开发人员设计自己的类的层次结构体系包含了抽象方法或抽象属性的类必须是抽 象类抽象类的这些成员交给派生类去实现密封类不允许被继承密封方法和密封 属性不允许被重载抽象和密封的概念是本章的难点希望读者认真掌握。