CSharp(二十六)类(Class)抽象类与抽象方法
一、从一个真实的问题开始
假设你正在开发一个游戏,里面有战士、法师、弓箭手三种职业。它们都有一个共同的行为——Attack(攻击),但每种职业的攻击方式不同:
- 战士:拿剑砍
- 法师:放火球
- 弓箭手:射箭
如果你写三个独立的类,代码会变成这样:
public class Warrior
{
public void Attack() { Console.WriteLine("战士挥剑砍击!"); }
}
public class Mage
{
public void Attack() { Console.WriteLine("法师释放火球术!"); }
}
public class Archer
{
public void Attack() { Console.WriteLine("弓箭手射出致命一箭!"); }
}
问题来了——如何统一管理这三种职业?比如你想把所有角色放进一个队伍里,然后让所有角色一起攻击,该怎么办?
这时候就需要抽象类登场了。
二、什么是抽象类?(用生活例子理解)
一句话定义:抽象类是一个"不完整的模板",它告诉你必须做什么,但不告诉你具体怎么做。
生活类比 1:电器说明书
你买了一台"抽象电器",说明书上写着:
- ✅ 必须有开关按钮(这是必须做的事)
- ❓ 但怎么开关,每个厂家的电器不一样(这是具体怎么做,留给子类实现)
生活类比 2:餐厅菜单
一张菜单写着"主食 + 汤 + 小菜"(这是模板),但具体是什么主食、什么汤,由每家分店自己决定。
生活类比 3:学校考试大纲
考试大纲规定:必须考语文、数学、英语(抽象方法),但每张试卷的具体题目不同(子类实现)。
核心思想:抽象类 = 制定规则 + 提供通用工具,把"个性化"的部分留给子类自由发挥。
三、抽象类的基本语法
3.1 声明抽象类
public abstract class 类名
{
// 可以有的东西 ↓
}
用 abstract 关键字修饰类即可。
3.2 抽象类的内部可以有什么?
| 成员类型 | 可以有吗? | 说明 |
|---|---|---|
| 字段(Field) | ✅ 可以 | 普通变量,存储数据 |
| 属性(Property) | ✅ 可以 | 普通属性 + 抽象属性 |
| 方法(Method) | ✅ 可以 | 普通方法 + 抽象方法 + 虚方法 |
| 构造函数 | ✅ 可以 | 但不能直接 new 这个类 |
| 事件(Event) | ✅ 可以 | |
| 索引器 | ✅ 可以 |
3.3 最重要的限制:不能 new
public abstract class Animal { }
// ❌ 编译错误!抽象类不能实例化
Animal a = new Animal();
// ✅ 正确:通过继承的子类来创建对象
public class Dog : Animal { }
Animal a = new Dog(); // 这样可以
为什么要这样设计? 因为抽象类本身就是"不完整"的——它里面的抽象方法没有代码。如果允许直接
new,调用抽象方法时程序就不知道该执行什么了。
四、抽象方法(Abstract Method)
4.1 什么是抽象方法?
抽象方法是一个只有签名、没有方法体的方法。它用分号 ; 直接结束,后面没有 { }。
public abstract class Animal
{
// 抽象方法:只声明,不实现
public abstract void MakeSound();
// 普通方法:有完整实现
public void Sleep()
{
Console.WriteLine("Zzz... 动物在睡觉");
}
}
4.2 子类必须重写抽象方法
public class Dog : Animal
{
// 必须用 override 重写抽象方法
public override void MakeSound()
{
Console.WriteLine("汪汪汪!");
}
}
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("喵喵喵!");
}
}
如果不重写会怎样? 编译器直接报错!
错误 CS0534: 'Dog' 未实现继承的抽象成员 'Animal.MakeSound()'
这是抽象类最大的优势——编译器强制子类实现规定的方法,防止你忘记。
4.3 完整示例:动物叫声系统
using System;
using System.Collections.Generic;
public abstract class Animal
{
public string Name { get; set; }
public Animal(string name)
{
Name = name;
}
// 抽象方法:每个动物必须实现
public abstract void MakeSound();
// 普通方法:所有动物通用
public void Eat()
{
Console.WriteLine($"{Name} 正在吃东西...");
}
}
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} 说:汪汪汪!");
}
}
public class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} 说:喵喵喵!");
}
}
public class Duck : Animal
{
public Duck(string name) : base(name) { }
public override void MakeSound()
{
Console.WriteLine($"{Name} 说:嘎嘎嘎!");
}
}
// ========== 使用 ==========
class Program
{
static void Main()
{
// 用抽象类类型做集合,实现多态
List<Animal> zoo = new List<Animal>
{
new Dog("旺财"),
new Cat("咪咪"),
new Duck("唐老鸭")
};
foreach (var animal in zoo)
{
animal.MakeSound(); // 每个动物发出不同声音
animal.Eat(); // 通用行为
}
}
}
/* 输出:
旺财 说:汪汪汪!
旺财 正在吃东西...
咪咪 说:喵喵喵!
咪咪 正在吃东西...
唐老鸭 说:嘎嘎嘎!
唐老鸭 正在吃东西...
*/
五、抽象属性(Abstract Property)
属性也可以被标记为抽象的。同样,只有声明,没有实现。
public abstract class Shape
{
// 抽象属性:子类必须实现
public abstract double Area { get; }
public abstract string Color { get; set; }
// 普通属性(可被子类继承使用)
public DateTime CreatedAt { get; } = DateTime.Now;
}
public class Circle : Shape
{
private double _radius;
private string _color;
public Circle(double radius)
{
_radius = radius;
}
// 实现抽象属性
public override double Area => Math.PI * _radius * _radius;
public override string Color
{
get => _color;
set => _color = value;
}
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
private string _color;
public override double Area => Width * Height;
public override string Color
{
get => _color;
set => _color = value;
}
}
六、抽象类中的构造函数
很多人误解:"抽象类不能 new,所以它没有构造函数?"
错! 抽象类可以有构造函数,但只有子类实例化时才会调用。
public abstract class Person
{
public string Name { get; }
public int Age { get; }
// 抽象类的构造函数
public Person(string name, int age)
{
Name = name;
Age = age;
Console.WriteLine($"抽象类构造函数被调用:{Name}, {Age}岁");
}
public abstract void Work();
}
public class Teacher : Person
{
public string Subject { get; }
// 子类构造函数中通过 base 调用抽象类构造函数
public Teacher(string name, int age, string subject)
: base(name, age) // ← 这里调用抽象类构造函数
{
Subject = subject;
Console.WriteLine("教师构造函数被调用");
}
public override void Work()
{
Console.WriteLine($"{Name} 老师正在教 {Subject}");
}
}
// 使用
class Program
{
static void Main()
{
Teacher t = new Teacher("张老师", 35, "数学");
t.Work();
}
}
/* 输出:
抽象类构造函数被调用:张老师, 35岁
教师构造函数被调用
张老师 老师正在教 数学
*/
理解:创建子类对象时,先执行抽象类的构造函数,再执行子类的构造函数。就像盖房子先打地基(抽象类),再盖楼上的房间(子类)。
七、抽象方法 vs 虚方法(Virtual Method)
这两个概念容易混淆,这里一次性彻底讲清楚:
| 对比维度 | abstract 抽象方法 |
virtual 虚方法 |
|---|---|---|
| 父类中必须有实现? | ❌不能有方法体,只有声明 | ✅必须有默认实现 |
| 子类是否必须重写? | ✅必须重写,否则编译报错 | ❌ 可选,不重写就用父类默认实现 |
| 用哪个关键字重写? | override |
override |
| 设计意图 | "你必须自己决定怎么做" | "我给你一个默认方案,你可以改" |
| 类比 | 考试必答题 | 附加题(可做可不做) |
代码对比
public abstract class Bird
{
// 抽象方法:每种鸟必须自己实现
public abstract void Fly();
// 虚方法:有默认实现,子类可以不重写
public virtual void Sing()
{
Console.WriteLine("叽叽喳喳...");
}
}
public class Eagle : Bird
{
// 必须实现抽象方法
public override void Fly()
{
Console.WriteLine("老鹰展翅高飞!");
}
// 可选:重写虚方法
public override void Sing()
{
Console.WriteLine("唳——!");
}
}
public class Penguin : Bird
{
// 必须实现抽象方法
public override void Fly()
{
Console.WriteLine("企鹅不会飞,但会游泳~");
}
// 不重写 Sing(),使用默认的"叽叽喳喳..."
}
选择建议:
- 如果必须让子类自己决定怎么做 → 用
abstract- 如果提供一个默认方案,子类爱改不改 → 用
virtual
八、回到最初的问题:用抽象类重构游戏角色
让我们用抽象类优雅地解决开头的游戏角色问题:
using System;
using System.Collections.Generic;
// ========== 抽象角色基类 ==========
public abstract class Character
{
public string Name { get; set; }
public int HP { get; set; }
public Character(string name, int hp)
{
Name = name;
HP = hp;
}
// 抽象方法:每个职业有不同的攻击方式
public abstract void Attack();
// 抽象方法:不同职业有不同技能
public abstract void UseSkill();
// 普通方法:所有角色通用的受伤逻辑
public void TakeDamage(int damage)
{
HP -= damage;
Console.WriteLine($"{Name} 受到 {damage} 点伤害,剩余血量:{HP}");
if (HP <= 0)
Console.WriteLine($"{Name} 已阵亡!");
}
}
// ========== 战士 ==========
public class Warrior : Character
{
public Warrior(string name) : base(name, 150) { }
public override void Attack()
{
Console.WriteLine($"{Name}(战士)挥动长剑,造成 30 点伤害!");
}
public override void UseSkill()
{
Console.WriteLine($"{Name}(战士)释放【旋风斩】!");
}
}
// ========== 法师 ==========
public class Mage : Character
{
public Mage(string name) : base(name, 80) { }
public override void Attack()
{
Console.WriteLine($"{Name}(法师)释放火球术,造成 45 点伤害!");
}
public override void UseSkill()
{
Console.WriteLine($"{Name}(法师)释放【暴风雪】!");
}
}
// ========== 弓箭手 ==========
public class Archer : Character
{
public Archer(string name) : base(name, 100) { }
public override void Attack()
{
Console.WriteLine($"{Name}(弓箭手)射出箭矢,造成 35 点伤害!");
}
public override void UseSkill()
{
Console.WriteLine($"{Name}(弓箭手)释放【箭雨】!");
}
}
// ========== 游戏运行 ==========
class Program
{
static void Main()
{
// 用抽象类类型统一管理所有角色
List<Character> team = new List<Character>
{
new Warrior("亚瑟"),
new Mage("甘道夫"),
new Archer("莱戈拉斯")
};
Console.WriteLine("=== 第一回合:全体攻击 ===");
foreach (var c in team)
c.Attack();
Console.WriteLine("\n=== 第二回合:全体释放技能 ===");
foreach (var c in team)
c.UseSkill();
Console.WriteLine("\n=== 敌人反击 ===");
foreach (var c in team)
c.TakeDamage(40);
}
}
运行结果:
=== 第一回合:全体攻击 ===
亚瑟(战士)挥动长剑,造成 30 点伤害!
甘道夫(法师)释放火球术,造成 45 点伤害!
莱戈拉斯(弓箭手)射出箭矢,造成 35 点伤害!
=== 第二回合:全体释放技能 ===
亚瑟(战士)释放【旋风斩】!
甘道夫(法师)释放【暴风雪】!
莱戈拉斯(弓箭手)释放【箭雨】!
=== 敌人反击 ===
亚瑟 受到 40 点伤害,剩余血量:110
甘道夫 受到 40 点伤害,剩余血量:40
莱戈拉斯 受到 40 点伤害,剩余血量:60
关键价值:现在要新增一个"牧师"职业,你只需要写一个新类继承
Character,不用改动任何现有代码。这就是开闭原则——对扩展开放,对修改关闭。
九、抽象类 vs 接口 — 终极对比
这是面试必考、工作必用的知识点。
| 对比维度 | 抽象类 (abstract class) |
接口 (interface) |
|---|---|---|
| 关键字 | abstract class |
interface |
| 能有字段吗 | ✅ 可以有 | ❌ 不能(只能属性、方法、事件、索引器) |
| 能有构造函数吗 | ✅ 可以有 | ❌ 不能 |
| 方法有实现吗 | ✅ 可以有(普通方法 / 虚方法) | ❌(C# 8.0 之前)/ ✅(C# 8.0+ 支持默认实现) |
| 继承数量 | 只能继承 1 个抽象类 | 可以实现多个接口 |
| 访问修饰符 | 成员可以有任何修饰符 | 成员默认 public,不能改 |
| 设计关系 | "是什么"(is-a) | "能做什么"(can-do) |
| 使用场景 | 有共同代码要复用,有共同特征 | 定义某种能力或契约 |
什么时候用抽象类?什么时候用接口?
用抽象类的场景:
- 多个类之间有共同代码可以复用(比如公共字段、公共方法逻辑)
- 类之间有天然的父子关系(狗是动物,汽车是交通工具)
- 需要构造函数来初始化公共数据
用接口的场景:
- 只想定义能力(能飞的、能游泳的、能比较大小的)
- 需要多继承(一个类可以有多个接口)
- 不同体系的类需要共享同一种行为
混合使用示例
// 抽象类:提供公共代码
public abstract class Animal
{
public string Name { get; set; }
public abstract void Eat();
public void Sleep() => Console.WriteLine($"{Name} 在睡觉...");
}
// 接口:定义额外能力
public interface IFlyable
{
void Fly();
}
public interface ISwimmable
{
void Swim();
}
// 子类:继承抽象类 + 实现多个接口
public class Duck : Animal, IFlyable, ISwimmable
{
public override void Eat()
{
Console.WriteLine($"{Name} 在吃小鱼");
}
public void Fly()
{
Console.WriteLine($"{Name} 飞起来了");
}
public void Swim()
{
Console.WriteLine($"{Name} 在水里游");
}
}
// 使用
Duck donald = new Duck { Name = "唐老鸭" };
donald.Eat(); // 来自抽象类
donald.Sleep(); // 来自抽象类(继承的通用方法)
donald.Fly(); // 来自接口 IFlyable
donald.Swim(); // 来自接口 ISwimmable
十、常见错误与注意事项
错误 1:尝试实例化抽象类
public abstract class Base { }
Base b = new Base(); // ❌ 编译错误 CS0144
错误 2:忘记 override 关键字
public abstract class Base
{
public abstract void DoSomething();
}
public class Derived : Base
{
// ❌ 编译错误!缺少 override
public void DoSomething() { }
// ✅ 正确写法
// public override void DoSomething() { }
}
错误 3:抽象方法不能是 private
public abstract class Base
{
// ❌ 编译错误!抽象方法不能是 private
// private abstract void DoSomething();
}
抽象方法必须被子类重写,
private意味着子类根本看不到它,所以矛盾了。
错误 4:抽象方法不能是 static
public abstract class Base
{
// ❌ 编译错误!抽象方法不能是 static
// public static abstract void DoSomething();
}
static方法属于类本身,不参与继承,而抽象方法就是为了被继承重写的。
错误 5:sealed 和 abstract 不能共存
// ❌ 编译错误!sealed 阻止继承,abstract 要求继承,矛盾
// public sealed abstract class Impossible { }
错误 6:抽象类中 virtual 和 abstract 的区别没搞清楚
public abstract class Base
{
// ✅ abstract: 子类必须实现
public abstract void MustDo();
// ✅ virtual: 有默认实现,子类可选重写
public virtual void OptionalDo()
{
Console.WriteLine("默认实现");
}
}
十一、课堂练习
练习 1:形状计算器
创建一个抽象类 Shape,包含:
- 抽象属性
Area(只读,计算面积) - 抽象属性
Perimeter(只读,计算周长) - 抽象方法
Describe()(输出形状描述)
然后创建 Circle(圆)和 Triangle(三角形)子类。
点击查看参考答案
public abstract class Shape
{
public abstract double Area { get; }
public abstract double Perimeter { get; }
public abstract void Describe();
}
public class Circle : Shape
{
public double Radius { get; set; }
public Circle(double radius) { Radius = radius; }
public override double Area => Math.PI * Radius * Radius;
public override double Perimeter => 2 * Math.PI * Radius;
public override void Describe()
{
Console.WriteLine($"这是一个半径为 {Radius} 的圆,面积:{Area:F2},周长:{Perimeter:F2}");
}
}
public class Triangle : Shape
{
public double A { get; set; }
public double B { get; set; }
public double C { get; set; }
public Triangle(double a, double b, double c)
{
A = a; B = b; C = c;
}
public override double Area
{
get
{
double s = (A + B + C) / 2;
return Math.Sqrt(s * (s - A) * (s - B) * (s - C));
}
}
public override double Perimeter => A + B + C;
public override void Describe()
{
Console.WriteLine($"这是一个边长分别为 {A}、{B}、{C} 的三角形,面积:{Area:F2},周长:{Perimeter:F2}");
}
}
练习 2:支付系统
创建一个抽象类 PaymentMethod,包含:
- 抽象方法
Pay(decimal amount)执行支付 - 抽象方法
Refund(decimal amount)执行退款 - 字段
Balance(余额)
创建 Alipay(支付宝)和 WeChatPay(微信支付)子类。
点击查看参考答案
public abstract class PaymentMethod
{
public string AccountName { get; set; }
protected decimal Balance;
public PaymentMethod(string accountName, decimal initialBalance)
{
AccountName = accountName;
Balance = initialBalance;
}
public abstract void Pay(decimal amount);
public abstract void Refund(decimal amount);
public void ShowBalance()
{
Console.WriteLine($"{AccountName} 当前余额:{Balance:C}");
}
}
public class Alipay : PaymentMethod
{
public Alipay(string account, decimal balance) : base(account, balance) { }
public override void Pay(decimal amount)
{
if (Balance >= amount)
{
Balance -= amount;
Console.WriteLine($"[支付宝] {AccountName} 支付 {amount:C} 成功");
}
else
Console.WriteLine($"[支付宝] {AccountName} 余额不足!");
}
public override void Refund(decimal amount)
{
Balance += amount;
Console.WriteLine($"[支付宝] {AccountName} 退款 {amount:C} 到账");
}
}
public class WeChatPay : PaymentMethod
{
public WeChatPay(string account, decimal balance) : base(account, balance) { }
public override void Pay(decimal amount)
{
if (Balance >= amount)
{
Balance -= amount;
Console.WriteLine($"[微信支付] {AccountName} 支付 {amount:C} 成功");
}
else
Console.WriteLine($"[微信支付] {AccountName} 余额不足!");
}
public override void Refund(decimal amount)
{
Balance += amount;
Console.WriteLine($"[微信支付] {AccountName} 退款 {amount:C} 到账");
}
}
十二、知识总结
一张图记住抽象类
┌─────────────────────────────────────────────┐
│ abstract class (抽象类) │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ abstract 成员(必须被子类实现) │ │
│ │ · 抽象方法 │ │
│ │ · 抽象属性 │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 普通成员(所有子类共享/可选重写) │ │
│ │ · 字段 │ │
│ │ · 普通方法 │ │
│ │ · 虚方法(virtual) │ │
│ │ · 构造函数 │ │
│ │ · 属性(非抽象) │ │
│ └─────────────────────────────────────┘ │
│ │
│ 不能 new,只能通过子类实例化 │
└─────────────────────────────────────────────┘
三句话记住核心要点
- 抽象类 = 模板:规定"必须做什么"(抽象方法) + 提供"公共工具"(普通方法)
- 不能
new:抽象类不完整,必须通过子类来创建对象 override是桥梁:子类用override实现抽象方法,编译器帮你检查是否遗漏
面试速记口诀
"抽象不实例,方法无身体,子类必须覆,编译来帮你"
- 抽象不实例 → 不能
new - 方法无身体 → 抽象方法没有
{ },只有; - 子类必须覆 → 子类必须
override所有抽象成员 - 编译来帮你 → 如果遗漏,编译器直接报错提醒
下一步学习建议:掌握了抽象类之后,建议继续学习 接口(Interface) 和 多态(Polymorphism),它们是面向对象编程中与抽象类紧密配合的核心概念。