首页 / C# 入门教程 / 抽象类与抽象成员

C# 入门教程

抽象类与抽象成员

本教程共 100 篇 · 第 43 篇 · 更新于 2026-07-31 · 约 11 分钟阅读

C#C# 入门教程面向对象抽象类

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}");

CircleSquare 各自实现了面积算法。调用方只认 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 虚方法

维度抽象方法虚方法
关键字abstractvirtual
是否有实现无,必须被重写有默认实现,可不重写
所在类必须 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,因为派生类必须能访问并重写它。一般用 protectedpublic

第二,派生类漏写某个抽象成员的实现,编译直接失败。这不是麻烦,而是编译器在帮你堵漏。

第三,以为抽象类一定比普通类“高级”。需要实例化、且行为完整时,普通类更合适。抽象类只用于“还必须被细化”的场景。

第四,在抽象类构造器里调用抽象方法。此时派生类还没初始化完,极易读到错误默认值。和重写一样,构造器内避免调用可被子类定制的成员。

第五,把抽象类当“工具类收纳箱”。抽象类是为继承设计的,若只是想聚合一些方法,普通静态类或接口也许更贴切。

最佳实践小结

  • 当“某些行为无法确定通用实现”时,用抽象方法强制子类补完。
  • 把固定的流程放进抽象类的普通方法(如模板方法),把变化点抽象出去。
  • 抽象类的构造器负责公共状态初始化,派生类用 base 传参。
  • is-a 关系且有共享状态时选抽象类,can-do 跨类型能力选接口。
  • 不要为了“显得抽象”而抽象,能完整实现就用普通类。

动手练一练

  1. 定义抽象类 Animal,含抽象方法 Eat() 与已实现方法 Breathe(),让 DogFish 各自实现 Eat(),用基类变量调用体会强制实现。
  2. 用模板方法写一个抽象类 GameInitialize()/Play()/End() 中把 Play() 设为抽象,其余实现默认逻辑,让 Chess 只实现 Play()
  3. 尝试 new Shape(),观察编译器如何拒绝,理解“抽象类不能实例化”。

要点速记

抽象类强调“是什么(is-a)关系”,是继承链里的“约束型基类”。它的强项是用编译期强制力逼你补完核心行为,并用模板方法沉淀不变流程。下一章我们讲另一种契约:接口,它强调“能做什么(can-do)”。