在学习 C# 面向对象编程的过程中,多态、抽象类、接口和结构体是四个绕不开的重要概念。它们构成了 C# 类型系统的基石,也是写出灵活、可扩展代码的关键。下面逐一梳理这些概念。

 一、多态

 1.1 什么是多态?

多态,字面意思是“多种形态”。在 C# 中,多态指的是同一个方法调用,在不同的对象上表现出不同的行为。你使用相同的方法名去调用不同类型的对象,它们各自执行自己特有的逻辑,返回不同的结果。这种机制的核心在于:调用代码不需要知道对象的具体类型,只需要知道对象具备某个方法即可。

1.2 多态的三种实现方式

第一种是虚方法与重写。 在父类中,用 virtual 关键字声明一个方法,表示这个方法可以被子类覆盖。子类中用 override 关键字重新编写该方法,提供自己的实现。当你用父类类型的变量去调用这个方法时,实际执行的是子类重写之后的方法。这个过程叫做运行时绑定,也就是在程序运行阶段才决定到底调用哪个版本。

第二种是抽象方法。在抽象类中,用 abstract 关键字声明一个方法,这个方法只有签名,没有方法体。它只规定了方法名、返回类型和参数,不提供任何实现。抽象方法的存在意义是强制子类必须提供自己的实现。子类继承抽象类后,必须用 override 关键字把所有的抽象方法一一实现,否则编译器会报错。

**第三种是接口多态。 接口定义了一组行为规范,不关心具体实现。任何类只要实现了某个接口,就必须把接口中声明的所有方法、属性和事件都完整地写出来。之后可以用接口类型的变量来引用任何实现了该接口的对象,调用接口中定义的方法。由于每个类的实现方式不同,同样的调用会触发不同的逻辑,这就是接口驱动的多态。

1.3 多态的价值

多态最大的好处是让代码变得灵活且易于扩展。当你需要新增一种类型时,只需要新建一个类并实现相应的方法,而不需要修改已有的调用代码。这符合软件工程中的开闭原则——对扩展开放,对修改关闭。同时多态也减少了重复代码,让程序的逻辑更加清晰集中。

 二、抽象类

 2.1 抽象类的本质

抽象类的关键词是“不完整”。它是一个不能被直接实例化的类,也就是说你不能用 new 来创建一个抽象类的对象。因为它代表的是一个模糊的、抽象的概念,不是一个具体的实体。抽象类描述了一类事物的共性特征和公共行为,但本身留有空白,需要子类去补全才能成为一个完整的类型。

2.2 抽象类的组成

抽象类中可以包含两类成员:

一类是抽象成员,包括抽象方法和抽象属性。它们只有声明,没有实现,存在的意义就是告诉子类“必须完成这些功能”。子类继承抽象类后,必须用 override 关键字把这些抽象成员全部实现,一个都不能少。

另一类是已经有具体实现的普通成员。这些成员可以直接被子类继承使用,子类也可以选择性地进行重写。此外抽象类还可以有构造函数,虽然自己不能被 new,但它的构造函数可以被子类通过 base 关键字调用,用来初始化父类定义的字段和属性。

2.3 抽象类的定位

抽象类表达的是“is-a”关系,即“子类是一种父类”。比如电动汽车是一种车,圆形是一种形状。当你需要为一组有共同特征的类提供基础代码,同时又不想让这个基础类本身被实例化时,抽象类就是最合适的选择。

三、接口

 3.1 接口是什么?

接口是一种纯契约、纯规范。它只回答一个问题:“这个东西能做什么?”,完全不关心“怎么做”。接口中声明的所有成员都只有签名,没有实现(从 C# 8.0 开始接口也支持默认实现,但那是进阶话题)。

接口隐藏了内部实现细节,只对外暴露行为规范。使用者只需要知道接口提供哪些功能,不需要了解这些功能在各类中具体如何实现。这样做的好处是调用方和实现方完全解耦,可以各自独立变化。

3.2 接口的核心优势:多实现

这是接口相比抽象类最大的优势。在 C# 中,一个类只能继承一个父类(包括抽象类),这叫单继承。但一个类可以实现多个接口,数量没有上限。

这解决了一个很现实的建模问题:一个类往往需要具备多种不同的行为能力。如果只能用单继承,很难同时表示这些能力。但通过实现多个接口,一个类可以自由组合各种行为。比如定义一个网路通信的类,它可以同时实现连接管理、数据加密、日志记录等多个接口,分别处理不同的职责。

3.3 接口 vs 抽象类:如何选择

抽象类表达的是“是什么”,接口表达的是“能做什么”。这是最根本的语义区别。

如果多个类之间有天然的层级继承关系,它们确实属于同一种事物,而且需要共享公共的代码和状态(字段、已实现的方法),那么用抽象类。比如猫、狗、鸟都有名字和年龄的属性,也都有进食的行为,抽象类可以统一管理这些共性。

但如果关注的只是某个跨类型的行为能力,比如“能支付”、“能序列化”、“能比较大小”,这些能力和类本身的身份无关,用接口就更合适。微信支付、支付宝、银行卡支付之间没有继承关系,但它们都能完成支付这件事——定义支付接口,让它们各自去实现即可。

简单总结:需要代码复用和体系化的继承关系,选抽象类。需要行为规范和灵活组合,选接口。

四、结构体

4.1 值类型 vs 引用类型

理解结构体的关键,在于理解值类型和引用类型的区别。

类属于引用类型。当你创建一个类的对象,它被分配在托管堆内存中,变量本身存储的只是这个对象在堆上的内存地址。当你把一个对象变量赋值给另一个变量时,拷贝的是这个地址,而不是对象本身。因此两个变量指向的是堆上同一个对象,通过其中任何一个变量修改对象的内容,另一个变量访问时也能看到变化。

结构体属于值类型。当你创建一个结构体的实例,它的数据直接存储在变量所在的栈空间或内联内存中。当你把一个结构体变量赋值给另一个变量时,会发生完整的数据拷贝——新的变量拥有一份独立的数据副本。修改副本不会影响原件,两者完全隔离。

4.2 结构体的特点与限制

结构体不支持继承。它不能继承其他结构体或类,也不能被其他结构体继承。不过结构体可以实现接口,这意味着它虽然不能参与继承链的层级关系,但仍然可以通过接口参与多态调用。

结构体通常不包含显式的无参数构造函数(早期 C# 版本限制),它的字段在创建时会自动初始化为类型默认值。此外结构体一般体积较小,适合用来表示轻量的数据单元,比如坐标点、颜色值、复数等数学或图形概念。

由于结构体是值类型,内存分配和回收的开销通常比引用类型低。在需要创建大量小对象且生命周期短的场景下,结构体的性能优势很明显。但如果结构体体积过大,每次赋值拷贝的开销也会增大,反而得不偿失。

 4.3 何时使用结构体

适合用结构体的情况:数据量小,通常是几个基本类型的组合;不需要继承;主要作为数据的载体使用;会大量创建且频繁拷贝;对性能有要求。

不适合用结构体的情况:需要继承和多态体系;数据量大、字段多;逻辑复杂、包含大量方法;需要通过引用语义共享修改同一个数据对象。

 五、总结

多态是最终的目标——希望代码足够灵活,能够以统一的方式处理不同类型的对象。

抽象类和接口是实现多态的两种主要手段。抽象类适合有直接层级关系的类型族,提供“模板”让子类来填补空白。接口适合跨类型的行为规范,定义“契约”让任意类来履行。

结构体则是另一个维度的补充。虽然它不能参与继承关系,但它是值类型的代表,在性能敏感、数据量小的场景下有不可替代的优势。

这四者各司其职,共同构成了 C# 类型系统的丰富体系。理解它们各自的定位和适用场景,是写出高质量 C# 代码的重要基础。

更多推荐