C#:类的继承
通读此篇大约需要十分钟。
- 使用符号
:
来实现继承。- 继承的含义:在代码中无需写出便可直接使用。
- 派生类会获得基类的所有成员。基类不会得到派生类独有的成员。
- 派生类不会获得基类的静态构造函数、实例构造函数、析构函数。
- 只有被标记为
virtual
,abstract
,override
的成员才可被重写。- 接口中无实体的函数(包括抽象函数),强制继承,即强制要求派生类重写。
- 接口中若有函数有实体,则不强制继承,无需重写。但若不重写,则无法调用。
- 阻止继承sealed。
本文将假设读者已经了解如何新建类,知晓类的属性,方法,会实例化类等知识。在此基础上,详细介绍C#继承、多态、接口等知识点。
本文引用其他前辈的地方,将通过[n]标识,并将出处原文网址置于文末。
一、继承基本知识点
- 继承,允许我们使用一个类来定义另一个类,使创建和维护程序变得更加容易。从概念上讲,派生类是基类的专门化。
- 已有的类称为基类。继承基类的类称为派生类。
- 一个派生类只能拥有一个基类,而一个基类可拥有多个派生类。和树的数据类型相同,一个子节点只能拥有一个父节点,但可拥有多个子节点。如图一,爷爷即为基类,下面的则为派生类。
- 所有类隐式继承
Object
类。
二、如何实现继承
实现继承的语法如下:
class BaseClass // 基类
{
}
class DerivedClass : BaseClass // 派生类
{
}
首先创建基类BaseClass
,之后在派生类声明时加上 :
即可。
三、派生类会继承到的成员
- 派生类会得到基类的所有成员。
- 基类不会得到派生类独有的成员。
**何谓继承到某个成员?继承到某个成员即不需要在代码中写便可直接使用。**比如,派生类可直接继承基类的普通方法,含义就是在派生类中可直接使用该方法。
派生类会隐式的获得基类的所有成员,可直接使用。但不包括静态构造函数、实例构造函数、析构函数。
简而言之,如图二,爷爷拥有成员小汽车,则派生类儿子也会拥有小汽车。儿子通过自己努力挣得小飞机,则爷爷不会有小飞机,而孙子会有小飞机。
class BaseClass //基类
{
public void Method1()
{
ConSole.WriteLine("基类的方法");
}
}
class DerivedClass : BaseClass // 派生类
{
public void Method2()
{
Console.WriteLine("派生类的方法");
}
static void Main(string[] args)
{
DerivedClass d = new DerivedClass(); // 实例化派生类
d.Method1(); // 派生类实例既可以使用基类的方法
d.Method2(); // 也能使用自己的方法
}
// 控制台输出:
// 基类的方法
// 派生类的方法
}
在上面这个例子中,基类拥有Method1()
方法,派生类的实例可直接使用。而如果创建基类实例,则不可调用派生类的Method2()
方法。
四、派生类不会继承到的成员(了解何为静态构造函数、实例构造函数和析构函数即可)
- 派生类不会得到基类的静态构造函数、实例构造函数、析构函数。
- 因为基类的此三种构造函数,在派生类中一定会被隐式调用,故无需继承。
- 静态构造函数用于初始化静态成员,只会被执行一次。
- 实例构造参数用于初始化普通成员。
- 析构函数用于在实例对象销毁时调用。
究其不可继承的原因,个人认为是毫无必要。首先为何要继承一个成员?因为希望在派生类中使用,继承可减少代码量。而基类的构造函数,在派生类一定会隐式的调用。所以并不需要继承。
1. 不会继承静态构造函数。
静态构造函数是用来初始化任何静态数据的。自动调用,且只会被调用一次。在创建第一个实例或引用任何静态成员之前自动调用。
class BaseClass
{
public static int numStatic;
static BaseClass() // 不可有参数(int num此类)、不可有修饰符(public此类)
{
numStatic = 10;
}
}
此处若没有静态构造函数初始化
numStatic
,则numStatic
会被自动初始化为默认值。int
类型即为0。
注意静态构造函数不可有参数,不可有修饰符,且不可直接访问。
为何了解即可?
因为静态构造函数初始化的是静态数据。而静态数据会隐式的被继承,即在派生类中可直接使用静态数据。
继上文例子。
class DerivedClass : BaseClass
{
static void Main(string[] args)
{
ConSole.WriteLine(BaseClass.numStatic); // 在这里使用毫无问题
}
// 控制台输出:
// 10
}
因为numStatic
是被隐式继承的,所以可以直接使用。
自然,如果在派生类中自己创建一个静态构造函数,自己重新给基类的静态数据赋值是可以的。但这就超出了继承的范畴,这里便不再讨论。
2. 不会继承实例构造函数
实例构造函数就是普通的构造函数。
为何了解即可?
通上文的静态构造函数一样,此构造函数就是可以初始化非静态数据。派生类在调用自己的构造方法时,会自动先调用基类的构造方法。
class BaseClass // 基类
{
// 基类实例构造方法
public BaseClass(int i){
ConSole.WriteLine("基类的方法:{0}", i);
}
}
class DerivedClass : BaseClass
{
public DerivedClass()
:base(i)
{
Console.WriteLine("派生类的方法");
}
static void Main(string[] args)
{
DerivedClass d = new DerivedClass(10);
}
// 控制台输出:
// 基类的方法:10
// 派生类的方法:10
}
如上面的例子,会自动先调用基类的构造方法。在有参构造函数中,使用:base()
来将参数传递至基类构造函数。
小知识点:创建一个类的时候,如果用户没有创建任何构造参数,则会自动的创建一个无参的构造函数。如果已经创建一个构造函数,例如用户创建了一个有一个参数的构造函数,则不会再自动创建无参构造函数。
3. 不会调用析构函数
析构函数同理,这里就不多赘述,只简单介绍如何使用。
class BaseClass
{
~BaseClass()
{
}
}
只需要添加~
符号即可,在销毁对象时调用析构函数。
五、重写基类成员 override
- 被
virtual
、abstract
、override
修饰的成员才可被重写。- 被
virtual
修饰的成员,可以选择重写也可以选择不重写直接调用。virtual
不可修饰任何静态成员。- 只有抽象类和接口中才可声明抽象方法。
- 抽象方法是隐式的虚拟方法。
- 抽象类中不止可以有抽象函数。
- 必须一次实现抽象类和接口中的所有抽象方法。
- 抽象的意义就是被实现。
- 接口中无实体的函数(包括抽象函数),强制继承,即强制要求派生类重写。
- 接口中若有函数有实体,则不强制继承,无需重写。但若不重写,则无法调用。
重写,即重新修改基类的函数、属性等。其意义在于专门化基类。
override
用于扩展或修改继承的方法、属性、索引器、事件的抽象、虚拟实现。
只有被virtual
、abstract
、override
修饰的成员才可被重写。无此三个关键字修饰的函数不可被重写。
1. 重写virtual
修饰的方法
virtual
关键字用于修改方法、属性、索引器、事件的声明,使它们在派生类中被重写。被virtual
修饰的成员,可以被任何继承此类的派生类所重写。
1.1 声明虚函数
在基类中用virtual
修饰方法Method1
。
class BaseClass
{
public virtual void Method1()
{
Console.WriteLine("基类的方法");
}
}
1.2 重写虚函数
在派生类中,重写虚函数,并使用。
class DerivedClass : BaseClass // 继承基类
{
// 重写基类的虚函数
public override void Method1()
{
Console.WriteLine("派生类的方法,不是基类的方法");
}
static void Main(string[] args)
{
DerivedClass d = new DerivedClass();
d.Method1(); // 调用重写的虚函数
}
// 控制台输出:
// 派生类的方法,不是基类的方法
}
当然这里重写虚方法并不强制。**也可以不重写,而直接调用。**接前面的例子
class DerivedClass : BaseClass // 继承基类
{
static void Main(string[] args)
{
DerivedClass d = new DerivedClass();
d.Method1(); // 直接调用基类的虚函数
}
// 控制台输出:
// 基类的方法
}
这里直接调用基类的虚函数。
这里注意,virtual
关键字不可修饰静态成员。
2. 重写abstract
修饰的方法
abstract
修饰的方法称为抽象方法,抽象方法只有方法的声明,没有方法的实现。只有抽象类和接口中才可声明抽象方法。抽象方法是隐式的虚拟方法。正因为抽象方法是隐式的虚拟方法,故抽象方法才可被重写。
2.1 重写抽象类中的抽象方法
抽象类用于需要在类中拥有少量抽象方法的情况。抽象类可视为可以包含抽象方法的特殊类。在普通的类前面加上abstract
关键字即为抽象类。
abstract class AbstractClass
{
}
在其中可以声明抽象方法。
abstract class AbstractClass
{
public abstract void AbstractMethod(); // 声明抽象方法,只有方法的声明,无方法的实现
}
抽象方法,只有方法的声明,无方法的实现,抽象方法将在其派生类中实现。
class DerivedClass : AbstractClass // 继承抽象类
{
static void Main(string[] args)
{
DerivedClass d = new DerivedClass();
d.AbstractMethod(); // 调用抽象方法
}
public override void AbstractMethod() // 重写抽象方法
{
Console.WriteLine("已实现的抽象方法");
}
// 控制台输出:
// 已实现的抽象方法
}
如果一个抽象类中有多个抽象方法,则需重写所有抽象方法。
为什么需要抽象呢?其意义就在于对于不同的情况可以有不同的实现,而不是像普通方法被写死。也正因为其意义是被实现,所以抽象类不应该被实例化。如果抽象类被实例化,则其中的抽象方法则无法被实现。接下来提到的接口也是同理,接口也同样不可被实例化。
2.2 重写接口中的抽象方法
- 接口中无实体的函数(包括抽象函数),强制继承,即强制要求派生类重写。
- 接口中若有函数有实体,则不强制继承,无需重写。但若不重写,则无法调用。
重写接口中的方法,虽属于继承,但重写的时候并不需要override
关键字。
知道如何声明接口,会在其中声明普通函数,并且在派生类中能够重写即可。其他证明步骤可略过。
由于C#更新版本的原因,导致接口部分做出了很多调整,这就导致了网络上教程存在互相矛盾的说法。这里便只叙述经过实机演示正确的部分,以及有意义的部分。这里只参考微软官方文档[2]——2020.04.14
接口中可以包含方法、属性、索引器和事件声明。在此部分只讨论和方法有关。
创建接口
interface ISamlpeInterface
{
}
使用interface
关键字创建接口,类可继承多个接口,接口可继承接口。接口默认为public
类型。一般习惯,接口名开头为大写字母I
。
接口中声明方法
interface ISamlpeInterface
{
void Method1(); // 无实体的普通函数,强制重写
}
所有函数均需被继承,且强制重写。
继承接口
这里的MethodAbstract
和Method1
均为无实体的函数,故强制重写,并且可以正常调用。
class Test : ISamlpeInterface // 继承接口
{
// 主函数
static void Main(string[] args)
{
Test t = new Test();
t.Method1();
}
// 强制要求实现无实体的抽象方法
public void Method1()
{
Console.WriteLine("我是Method1");
}
// 控制台输出
// 我是Method1
}
其他用法知识点使用的较少,了解即可。
其他用法
在接口中,可以拥有以下两类函数。有实体的函数,和无实体的函数。
interface ISamlpeInterface
{
abstract void MethodAbstract(); // 抽象函数不可有实体,派生类中强制重写
void Method1(); // 无实体的普通函数,强制重写
void Method2() // 有实体的普通函数,不强制重写。若不重写,无法调用
{
Console.WriteLine("我是Method2");
}
static void MethodStatic() // 静态方法必须有实现的实体,不强制重写。若不重写,无法调用
{
Console.WriteLine("我是MethodStatic");
}
}
class Test : ISamlpeInterface
{
static void Main(string[] args)
{
Test t = new Test();
t.MethodAbstract(); // 调用抽象方法
t.Method1();
t.Method2(); // 报错!!!!
t.MethodStatic(); // 报错!!!!
}
// 强制要求实现抽象方法
public void MethodAbstract()
{
Console.WriteLine("我是MethodAbstract");
}
// 强制要求实现无实体的抽象方法
public void Method1()
{
Console.WriteLine("我是Method1");
}
// 控制台输出
// 我是MethodAbstract
// 我是Method1
}
因为Method2
和MethodStatic
两个函数未强制重写,所以调用将会报错。**如果想使用这两个函数,则必须人为的实现。**就如同下面这个例子。此例子接上面的接口。
class Test : ISamlpeInterface
{
static void Main(string[] args)
{
Test t = new Test();
t.Method2();
Test.MethodStatic();
}
// 用户自己重写,才可调用
public void Method2()
{
}
// 用户自己重写,才可调用
static void MethodStatic()
{
}
// 控制台输出
//
}
是的,控制台是毫无输出,在接口中写的函数的实体毫无作用。故在接口中声明有实体的函数,和直接在派生类中自己写一个函数是完全等效的。
本人尚未发现在接口的方法中添加实体有何作用,若有错误,还请各位前辈斧正,还请前辈不吝指教。
六、阻止被继承
如果不希望自己的类、接口被别的类、接口继承该如何操作呢?
只需要添加sealed
关键字即可。
sealed class BaseClass // 基类
{
}
class DerivedClass : BaseClass // 派生类继承则会报错
{
}
这里继承则会报错。
参考文献
[1] : 数据结构-树(tree)——yang蜗牛
[2] : 微软.NET文档