抽象类与抽象成员
本教程共 100 篇 · 第 43 篇 · 更新于 2026-07-31 · 约 11 分钟阅读
43. 抽象类与抽象成员
本节目标:用
abstract定义只能被继承、不能直接实例化的类,并掌握模板方法思路。
什么时候用抽象类
基类里有些方法“说不清具体怎么做”,例如“图形”的面积。不同的图形算法完全不同,基类根本无法给出通用实现。这时把方法声明为抽象的(abstract),强制派生类去补完。
包含抽象成员的类本身也必须标 abstract,这就是抽象类(abstract class)。它像一张“未画完的蓝图”,必须有人接着画完才能用。
Note抽象类的两个“不能”:①不能被
new直接实例化;②抽象成员没有方法体。但抽象类里完全可以有其他已经实现好的普通成员,派生类直接白嫖。
为什么需要抽象类
把这个问题想透。普通基类的问题在于:它“允许”你不去实现某个行为——你完全可以什么都不写,基类给个默认实现凑合。但有些行为根本就不该有默认,比如“图形面积”,给个 return 0 是错的,给个 return 0 还会误导调用方。
抽象方法用编译器的强制力解决这件事:你只要继承了抽象类,就必须把抽象成员补完,否则编译不过。它把“漏实现”从运行期隐患变成编译期错误,等于让编译器替你守门。
abstract 方法没有方法体
抽象方法只有签名,没有 {} 实现,以分号结尾。派生类必须用 override 提供具体实现。
namespace CSharpDemo;
abstract class Shape
{
public abstract double Area();
public string Name { get; set; } = "图形";
}
class Circle : Shape
{
public double Radius { get; set; }
public override double Area() => Math.PI * Radius * Radius;
}
class Square : Shape
{
public double Side { get; set; }
public override double Area() => Side * Side;
}
Shape shape = new Circle { Radius = 2 };
Console.WriteLine($"{shape.Name} 面积:{shape.Area():F2}");
Circle 和 Square 各自实现了面积算法。调用方只认 Shape,不必关心背后是圆还是方。
抽象属性也能“强制”
抽象不只是方法,属性也可以抽象。派生类必须用 override 给出具体属性或字段支撑。
namespace CSharpDemo;
abstract class Vehicle
{
public abstract int Wheels { get; }
}
class Bike : Vehicle
{
public override int Wheels => 2;
}
Note抽象类里可以有普通成员(字段、已实现的方法)。只有标了
abstract的成员才强制派生类实现。
抽象类也有构造器
抽象类不能被 new,但它可以有构造器。派生类构造时通过 base 调用基类构造,传递公共参数。
namespace CSharpDemo;
abstract class Shape
{
protected string _name;
protected Shape(string name) => _name = name;
public abstract double Area();
}
class Circle : Shape
{
public double Radius { get; set; }
public Circle() : base("圆") { }
public override double Area() => Math.PI * Radius * Radius;
}
这点和普通基类完全一样:构造器不继承,但派生类必须保证基类部分被正确初始化。
Tip抽象类的构造器常用于统一初始化“公共状态”(如名字、编号)。派生类构造时
: base(...)把公共参数交给父类,自己只管特有字段。
抽象方法 vs 虚方法
| 维度 | 抽象方法 | 虚方法 |
|---|---|---|
| 关键字 | abstract | virtual |
| 是否有实现 | 无,必须被重写 | 有默认实现,可不重写 |
| 所在类 | 必须 abstract 类 | 普通或抽象类均可 |
| 派生类义务 | 必须 override | 可选 override |
简单记:抽象是“必须补完”,虚方法是“可以替换”。
Note二者也能组合:抽象类里既有
abstract成员(强制补完),也有virtual成员(可选替换)。这样基类既能“卡死核心契约”,又能“开放可定制点”。
抽象类不能实例化
你不能直接 new Shape(),编译器会拒绝。抽象类是为继承而生的“半成品”,必须等派生类补齐抽象成员才能变成可用的具体类型。
模板方法(Template Method)思路
抽象类真正的威力,是“定好流程骨架,把可变步骤留给子类”。这就是模板方法模式(template method pattern)。
namespace CSharpDemo;
abstract class Report
{
// 模板方法:流程固定,步骤可变
public void Generate()
{
Open();
WriteBody();
Close();
}
private void Open() => Console.WriteLine("打开文档");
private void Close() => Console.WriteLine("关闭文档");
protected abstract void WriteBody();
}
class DailyReport : Report
{
protected override void WriteBody() => Console.WriteLine("写入当日数据");
}
new DailyReport().Generate();
Generate 的调用顺序由基类锁死,子类只管实现 WriteBody。新增报告类型时,流程永远不会被写错。
Tip抽象类的价值不只是“不能实例化”,更在于“把不变的流程沉淀到基类,把变化的点留给子类”。模板方法模式是这一思想最经典的体现:主流程你别动,细节交给我。
抽象类与接口的取舍
| 维度 | 抽象类 | 接口 |
|---|---|---|
| 关系语义 | is-a(是什么) | can-do(能做什么) |
| 能否有字段/构造器 | 能 | 不能 |
| 继承数量 | 单继承 | 可实现多个 |
| 适合场景 | 相关类型共享状态与流程 | 跨类型能力契约 |
二者不是二选一的对立关系。常见做法是:用抽象类表达“是什么”,用接口补充“能做什么”。例如 Bird 继承 Animal(is-a),同时实现 IFlyable(can-do)。
Tip记一句口诀:要“身份”用抽象类,要“能力”用接口。需要多份能力就叠加多个接口。下一章会专门讲接口。
抽象类在不同语言中的客观差异
Java 的抽象类和 C# 高度相似(abstract 关键字、不能实例化、强制实现抽象方法)。C++ 的“纯虚函数”(= 0)扮演同样的角色,但 C++ 支持多继承,抽象基类可以组合多个,而 C# 一个类只能继承一个抽象类。Python 没有关键字级的 abstract,而是靠 abc 模块配合装饰器实现,属于“约定式”而非“强制式”。
理解差异有助于你跨语言迁移,但记住:C# 的抽象类靠编译器强制,比“约定式”更不容易漏实现。
新手常见坑
第一,抽象方法不能是 private,因为派生类必须能访问并重写它。一般用 protected 或 public。
第二,派生类漏写某个抽象成员的实现,编译直接失败。这不是麻烦,而是编译器在帮你堵漏。
第三,以为抽象类一定比普通类“高级”。需要实例化、且行为完整时,普通类更合适。抽象类只用于“还必须被细化”的场景。
第四,在抽象类构造器里调用抽象方法。此时派生类还没初始化完,极易读到错误默认值。和重写一样,构造器内避免调用可被子类定制的成员。
第五,把抽象类当“工具类收纳箱”。抽象类是为继承设计的,若只是想聚合一些方法,普通静态类或接口也许更贴切。
最佳实践小结
- 当“某些行为无法确定通用实现”时,用抽象方法强制子类补完。
- 把固定的流程放进抽象类的普通方法(如模板方法),把变化点抽象出去。
- 抽象类的构造器负责公共状态初始化,派生类用
base传参。 - is-a 关系且有共享状态时选抽象类,can-do 跨类型能力选接口。
- 不要为了“显得抽象”而抽象,能完整实现就用普通类。
动手练一练
- 定义抽象类
Animal,含抽象方法Eat()与已实现方法Breathe(),让Dog、Fish各自实现Eat(),用基类变量调用体会强制实现。 - 用模板方法写一个抽象类
Game,Initialize()/Play()/End()中把Play()设为抽象,其余实现默认逻辑,让Chess只实现Play()。 - 尝试
new Shape(),观察编译器如何拒绝,理解“抽象类不能实例化”。
要点速记
抽象类强调“是什么(is-a)关系”,是继承链里的“约束型基类”。它的强项是用编译期强制力逼你补完核心行为,并用模板方法沉淀不变流程。下一章我们讲另一种契约:接口,它强调“能做什么(can-do)”。