1. 入门
抽象类 —— 就是你不能直接用new来创建的类。它的作用是让你在它的基础上写出更具体的类。可以把它当成一种模板或者“蓝图”,它规定了子类都要有啥结构和行为,但具体细节让子类自己去实现。
来个比喻更好懂:假如你有个“交通工具”的概念。现实生活中没人骑着抽象的“交通工具”到处跑(除非你在脑补)。有人开车,有人坐船,有人坐飞机。抽象类就是你的“交通工具”蓝图,它规定了所有交通工具都得有个核心功能——能移动(Move),但怎么移动,得让子类自己说了算。
// 抽象类示例
public abstract class Transport
{
public string Model { get; set; }
// 抽象方法——没有实现
public abstract void Move();
}
你要是想创建Transport类型的对象,编译器会直接报错!你可以试试,编译器会很快告诉你:“不能创建抽象类的实例”。
什么时候用抽象类?为啥要用?
- 想把一堆相似对象的共性和行为都集中到一个地方。
- 需要定个“统一标准”:比如“所有子类都得能移动”。
- 有些方法可以写通用实现,但有些逻辑必须让子类自己补充。
抽象类是设计大型系统架构的神器,适合那种有一堆通用“功能”,但每个子类又有自己特殊实现的场景。
2. 抽象类和普通类的区别
这里经常有人搞混,咱们来理一理!
抽象类 vs 普通类
- 普通类可以直接用new创建对象,所有方法都能有实现。
- 抽象类:
- 不能直接创建(类声明里有abstract)
- 可以有实现的方法,也可以有没实现(没方法体)的抽象方法
- 可以有字段、属性、构造函数,甚至静态方法
public abstract class Animal
{
public string Name { get; set; }
public void Sleep()
{
Console.WriteLine("动物在睡觉!");
}
public abstract void MakeSound();
}
3. 抽象方法和属性
抽象方法
抽象方法——只能在抽象类里声明,没有方法体。子类必须自己实现它,怎么写随你。
public abstract class Animal
{
public string Name { get; set; }
// 抽象方法
public abstract void MakeSound();
}
抽象属性
你也可以声明抽象属性,这样所有子类都必须实现它:
public abstract class Shape
{
// 抽象属性
public abstract double Area { get; }
}
4. 继承抽象类
抽象类可以有抽象方法,也可以有实现的方法。子类必须实现所有抽象方法和属性,否则它自己也得是抽象类。
咱们来扩展下之前讲过的“动物应用”:
public abstract class Animal
{
public string Name { get; set; }
public void Sleep()
{
Console.WriteLine($"{Name} 睡得很香!");
}
public abstract void MakeSound();
}
现在写个普通(非抽象)类:
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine($"{Name} 说:汪!");
}
}
你要是不实现MakeSound(),编译器会报错。
图解:带抽象类的类继承结构
+---------------------+
| Animal (abstract) |
+---------------------+
| Name: string |
| + Sleep() |
| + MakeSound()* |
+---------------------+
^
|
+------+-------+
| |
Dog Cat
* MakeSound() —— 抽象方法
5. 多态实战:抽象怎么用在实际项目里
来,咱们在小项目里用用这些知识。假设你有个动物列表——比如有狗和猫。
List<Animal> animals = new List<Animal>
{
new Dog { Name = "Rex" },
new Cat { Name = "Luna" }
};
foreach (var animal in animals)
{
animal.MakeSound(); // 多态调用:各自实现
animal.Sleep(); // 调用基类实现的方法
}
Cat类实现也差不多:
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine($"{Name} 说:喵!");
}
}
实际项目里的好处
抽象类在大型应用架构里用得超多。比如画图软件里通常有个抽象类Shape,有Draw()方法和Area属性。具体的图形(比如矩形、圆形)用自己的公式实现这些抽象方法。这样你可以随便加新图形,程序逻辑不用重写一遍。
6. 抽象类的特点和限制
不能创建实例
编译器绝对不让你这么干:
Animal a = new Animal(); // 错误!
只有子类(具体类)能被实例化。
抽象类可以包含:
- 抽象方法声明(没实现)
- 实现的方法(有方法体)
- 属性和字段(可以是抽象的,也可以有实现)
- 构造函数——没错,抽象类也能有构造函数,但只能被子类调用,不能直接new出来用。
抽象类支持继承
你可以搞一整套抽象层级:
public abstract class Creature
{
public abstract void Live();
}
public abstract class Animal : Creature
{
public abstract override void Live();
public abstract void MakeSound();
}
如果继承链上有类没实现所有抽象方法,它自己也得声明成抽象类!
7. 抽象类和构造函数
抽象类可以有构造函数,这些构造函数会在子类构造函数里被调用。
重点:抽象类的构造函数一般声明成protected,这样就明确只能被子类用,不能直接实例化。
public abstract class Animal
{
public string Name { get; set; }
// 构造函数声明为protected
protected Animal(string name)
{
Name = name;
Console.WriteLine($"创建了名叫{name}的动物");
}
public abstract void MakeSound();
}
public class Dog : Animal
{
public Dog(string name) : base(name)
{
// base(name)会调用Animal的构造函数
Console.WriteLine("创建了一只狗");
}
public override void MakeSound()
{
Console.WriteLine($"{Name} 说:汪!");
}
}
为啥用protected构造函数?
- 意图明确:protected表示构造函数只给子类用
- 防止误用:虽然编译器本来也不让你new,但protected构造函数让API更清晰,别人一看就懂
- 好习惯:符合封装和API设计原则
你也可以用public构造函数,但protected更能表达它的用途。
8. 抽象方法和虚方法的对比
抽象方法其实就是特殊的虚方法(它们本质上总是virtual,虽然不用写这个词),但抽象方法不能有实现。如果你想让子类可以重写(但不是必须),用virtual。如果必须重写——就用abstract。
public abstract class Worker
{
// 必须实现
public abstract void Work();
// 可以重写,也可以用默认实现
public virtual void Rest()
{
Console.WriteLine("标准休息!");
}
}
| 抽象方法 | 虚方法 | |
|---|---|---|
| 基类有没有实现 | 没有,只有声明(没方法体) | 有,默认实现 |
| 能在哪声明 | 只能在抽象类里 | 任何非sealed类,包括抽象类 |
| 子类必须重写吗 | 第一个非抽象子类必须重写 | 不一定。可以重写,也可以用基类实现 |
| 能不能直接调用 | 不能(没方法体) | 能(会执行基类版本) |
| 主要用途 | 强制子类写自己的实现。定义“标准” | 让子类有机会改行为,按需重写 |
9. 抽象类在你项目里的用法
继续完善我们的学习项目。现在我们需要一个处理用户输入的抽象类(比如各种表单)。
public abstract class InputForm
{
public string Title { get; set; }
// 抽象方法,实现输入处理流程
public abstract void ShowAndHandleInput();
}
public class LoginForm : InputForm
{
public override void ShowAndHandleInput()
{
Console.WriteLine($"=== {Title} ===");
Console.Write("请输入登录名: ");
string login = Console.ReadLine();
Console.Write("请输入密码: ");
string password = Console.ReadLine();
Console.WriteLine("认证完成!");
}
}
public class RegistrationForm : InputForm
{
public override void ShowAndHandleInput()
{
Console.WriteLine($"=== {Title} ===");
Console.Write("请设置登录名: ");
string login = Console.ReadLine();
Console.Write("请设置密码: ");
string password = Console.ReadLine();
Console.WriteLine("注册完成!");
}
}
用法:
InputForm[] forms = new InputForm[]
{
new LoginForm { Title = "登录" },
new RegistrationForm { Title = "注册" }
};
foreach (var form in forms)
{
form.ShowAndHandleInput();
}
这个套路在你程序越写越大时会越来越有用。抽象类是搭建可扩展系统的好基石,能让你做到DRY和SOLID(不重复自己,写模块化、易扩展的代码)这些好习惯。
10. 用抽象类和方法时常见的坑
错误1:忘了实现抽象方法。
如果子类没实现所有抽象方法和属性,编译器会报错。这很常见,尤其是用别人的代码或者继承链很长的时候。一定要仔细检查,别漏了啥重要的。
错误2:想把方法同时声明成abstract和static。
这样不行。抽象方法是给实例用的,静态方法根本不需要实例。这俩思路冲突,编译器不会让你这么写。
错误3:“偷懒”不实现所有抽象成员。
你要是不想实现所有抽象方法,那你这个类也得声明成abstract。这不算错,但可能说明你的架构有点绕。可以考虑把部分实现放到中间的抽象类里,让后面的子类更轻松。
GO TO FULL VERSION