计算机程序,在很大的程度上是为了描述和解决现实问题。在面向对象语言中的类很好的采用了人类思维中抽象和分类的方法,类和对象的关系很好的反映了个体与同类群体的共同特征的关系。但是在诸多共同点之下还是存在着些许差异。于是面向对象语言中设计了继承机制,允许我们在保持原有类特性的基础上,进行拓展。由于类的继承和派生机制的引入,使得代码的重用性和可扩充性大大提高。利用这个机制我们还可以站在巨人的肩膀上就行开发---利用别人写好的类进行扩充,这样又可以提高我们的开发效率。在派生新类的过程一般来说有三个步骤:吸收基类成员,改造基类成员,添加新成员。
继承的概念说完,我们先看看C#中有哪些继承类型(不多说直接上图)。
在粗略的了解C#的继承类型之后,现在我们就来详细的看看。
在C#中继承方式默认为公共继承(public),所以不需要在基类名前添加限定符,这点与C++有些不同。在C++中有三种继承方式(public, protected, private),相比之下C#的继承方式就显得简洁多了。现在我们看看具体是如何继承的,语法如下:
①类与类之间的继承
[访问权限修饰符] class [自定义类名] : [基类名] { //类体 }
②类(结构)与接口之间的继承
[访问权限修饰符] class [自定义类名] : [接口1的名称],[接口2的名称] { //类体 }
(如果是结构,只要将关键字class换成struct就变成结构继承接口)
③混合继承
[访问权限修饰符] class [自定义类名] : [基类名], [接口1的名称],[接口2的名称] { //类体 }
(注意:如果在类定义中没有指定基类,C#编译器就和假定System.Object为基类,可以简写为object)
我们之前说派生新类有三个步骤,现在我们就展开的说一下:
1、对于吸收基类成员,其实就是对基类继承下来的成员不做任何改变。
2、对于改造基类成员:
原有类方法的处理方式对新类不合适,此时我们就需要在新类中对继承来的方法进行改造。又根据改造方式的不同可分为:虚方法和隐藏方法。
<1>虚方法
对从基类继承来的方法,进行改造(也就是重写)。
具体步骤:先把基类的函数声明为virtual,语法如下:
[访问权限修饰符] virtual [函数返回值类型] 函数名(参数表) { //函数体 }
在子类中,重写该函数时,使用override关键字,语法如下:
[访问权限修饰符] override [函数返回值类型] 函数名(参数表) { //重写的函数体 }
(注意:成员字段和静态函数都不能声明为virtual)
<2>隐藏方法
隐藏方法其实不能算做真正意义上的改造,只是在子类中重新定义了一个与原方法同名的方法。然后在把继承的方法隐藏。语法如下:
[访问权限修饰符] new [函数返回值类型] 函数名(参数表) { //重写的函数体 }
现在我们看看,这两种方式的改造有什么区别。比如说我们要继承的方法为A。那么可以这样简单的理解,在子类中,虚方法实际上是将继承的方法A删除,然后再用子类中新定义的方法A去替代原有的方法A。换而言之,使用虚方法(重写)时,子类中就只有一个方法A(这个方法A也就是我们重写的)。用上面的逻辑我们就可以得到:使用隐藏方法时,子类中有两个方法A(一个是从父类继承的,一个是在子类中新定义的)。那么在向上转型的时,我们在调用方法A的过程中,对于重写,我们调用的将是子类的方法A。对于隐藏方法,我们将是调用父类的方法A。
3、添加新成员,这个步骤是对原有类进行扩充,其添加成员的方式和普通类没有区别。
新类中,我们有时需要调用方法的基类版本,语法:base.<方法名称>(参数列表)
(注意:这个语法可以调用基类中的任何方法,不必从同一个方法的重载中去调用)
抽象类和抽象函数,这个有点不好解释。我们以汽车为例:抽象类就相当于所有车共有的属性。继承这些属性,对其进行扩展,那么我们就可以得到各种各样的车了。然而抽象函数相当于车具有的属性。声明抽象类和抽象函数使用abstract。
(注意:抽象类不能实例化,抽象函数不能直接实现,必须要在非抽象类的派生类进行重写)
我们有时出于商业目的或者是其他原因不希望自己编写的类,在其作用域之外的地方被调用。于是我们就引入了密封类和密封方法的概念。我们使用关键字sealed来声明密封类或密封方法。对于类,则表示不能继承该类,对于方法,表示不能重写。
在之前我们知道了单个类的构造函数工作过程现在我们看看派生类的构造函数,先上图
构造函数的执行顺序,最先调用的总是基类的构造函数。(其实和递归调用过程十分类似)
(注意:如果在子类中显式的调用基类的构造函数,那么编译器默认调用的基类构造函数为无参的。也就是说如果基类中不存在无参构造函数,那么编译器就会报错。)
我们回过头来再看看接口,其实你会发现,接口其实与抽象类有一点类似。不同之处在于接口中只能包含成员签名。接口不能有构造函数,也不能有字段,也不允许包含运算符重载。命名规则,定义接口名称时,以大写字母I开头,这样我们一见到接口名称就知道这是接口。另外我们使用interface关键字定义接口其语法如下:
[访问权限修饰符] interface 接口名 { //接口体 }
在接口之间的继承,实际上就是一个加法的过程,也就是说继承父接口之后得到的子接口,其内部一定是大于等于父接口。接口还有一个强大的功能:接口引用。为什么说它强大呢?因为接口引用可以引用任何实现该接口的类。比如:我们可以构造数组,让数组的每个元素都是不同的类:
//ITest为接口,CTest1, CTest2为实现该接口的类 ITest[] t = new ITest[2]; t[0] = new CTest1(); t[1] = new CTest2();
这样的话,数组的每个元素都成为了不同类的类对象。
(注意:接口引用不能引用除上述之外的的类,否则编译器将会报错。)
利用这个特点,我们还可以有这样的用途:在某个方法中需要CTest1或CTest2作为参数时,其方法内容一致,那么这时,我们只要这样:
public void fun(ITest temp) { //函数体 }
那么此时参数可以是CTest1,也可以是CTest2。换而言之,我们不必知道其引用的是什么类型,我们只需知道其对象实现了ITest的接口就够了。
以上就是C#中的继承机制。