简明教程——C#:类的继承

C#:类的继承

通读此篇大约需要十分钟。

  • 使用符号:来实现继承。
  • 继承的含义:在代码中无需写出便可直接使用。
  • 派生类会获得基类的所有成员。基类不会得到派生类独有的成员。
  • 派生类不会获得基类的静态构造函数、实例构造函数、析构函数。
  • 只有被标记为virtual, abstract, override的成员才可被重写。
  • 接口中无实体的函数(包括抽象函数),强制继承,即强制要求派生类重写。
  • 接口中若有函数有实体,则不强制继承,无需重写。但若不重写,则无法调用。
  • 阻止继承sealed。

本文将假设读者已经了解如何新建类,知晓类的属性,方法,会实例化类等知识。在此基础上,详细介绍C#继承、多态、接口等知识点。

本文引用其他前辈的地方,将通过[n]标识,并将出处原文网址置于文末。

一、继承基本知识点

  1. 继承,允许我们使用一个类来定义另一个类,使创建和维护程序变得更加容易。从概念上讲,派生类是基类的专门化。
  2. 已有的类称为基类。继承基类的类称为派生类。
  3. 一个派生类只能拥有一个基类,而一个基类可拥有多个派生类。和树的数据类型相同,一个子节点只能拥有一个父节点,但可拥有多个子节点。如图一,爷爷即为基类,下面的则为派生类。
  4. 所有类隐式继承Object类。

在这里插入图片描述

Fig. 1. 树——数据结构[1]

二、如何实现继承

实现继承的语法如下:

class BaseClass    // 基类
{   
    
}

class DerivedClass : BaseClass   // 派生类
{
    
}

首先创建基类BaseClass,之后在派生类声明时加上 :即可。

三、派生类会继承到的成员

  • 派生类会得到基类的所有成员。
  • 基类不会得到派生类独有的成员。

**何谓继承到某个成员?继承到某个成员即不需要在代码中写便可直接使用。**比如,派生类可直接继承基类的普通方法,含义就是在派生类中可直接使用该方法。

派生类会隐式的获得基类的所有成员,可直接使用。但不包括静态构造函数、实例构造函数、析构函数

简而言之,如图二,爷爷拥有成员小汽车,则派生类儿子也会拥有小汽车。儿子通过自己努力挣得小飞机,则爷爷不会有小飞机,而孙子会有小飞机。

在这里插入图片描述

Fig. 2. 继承后成员关系
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

  • virtualabstractoverride修饰的成员才可被重写。
  • virtual修饰的成员,可以选择重写也可以选择不重写直接调用。
  • virtual不可修饰任何静态成员。
  • 只有抽象类和接口中才可声明抽象方法。
  • 抽象方法是隐式的虚拟方法。
  • 抽象类中不止可以有抽象函数。
  • 必须一次实现抽象类和接口中的所有抽象方法。
  • 抽象的意义就是被实现。
  • 接口中无实体的函数(包括抽象函数),强制继承,即强制要求派生类重写。
  • 接口中若有函数有实体,则不强制继承,无需重写。但若不重写,则无法调用。

重写,即重新修改基类的函数、属性等。其意义在于专门化基类。

override用于扩展或修改继承的方法、属性、索引器、事件的抽象、虚拟实现

只有被virtualabstractoverride修饰的成员才可被重写。无此三个关键字修饰的函数不可被重写。

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();    // 无实体的普通函数,强制重写

}

所有函数均需被继承,且强制重写。

继承接口

这里的MethodAbstractMethod1均为无实体的函数,故强制重写,并且可以正常调用。

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

}

因为Method2MethodStatic两个函数未强制重写,所以调用将会报错。**如果想使用这两个函数,则必须人为的实现。**就如同下面这个例子。此例子接上面的接口。

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文档

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值