541 lines
10 KiB
Markdown
541 lines
10 KiB
Markdown
概念:
|
|
使用 `abstract` 关键字修饰的类,叫做==抽象类==
|
|
使用 `abstract` 关键字修饰的方法,叫做==抽象方法==
|
|
|
|
简单理解:
|
|
抽象类像一份“不完整的父类模板”
|
|
它可以规定子类必须有什么能力,但具体怎么做,交给子类自己实现
|
|
|
|
补充:
|
|
有些教程会把“抽象方法”叫作“抽象函数”,初学阶段可以先理解成同一个意思
|
|
|
|
通常用法:
|
|
1. 多个类有共同身份,但父类本身不应该被直接创建对象
|
|
2. 父类能提供一部分公共代码
|
|
3. 父类还想强制子类必须实现某些方法
|
|
4. 配合[[继承]]和[[多态vob|多态]],让调用方通过父类统一调用子类行为
|
|
|
|
----
|
|
# 语法
|
|
抽象类:
|
|
```csharp
|
|
abstract class 类名
|
|
{
|
|
|
|
}
|
|
```
|
|
|
|
抽象方法:
|
|
```csharp
|
|
abstract class 类名
|
|
{
|
|
public abstract void 方法名();
|
|
}
|
|
```
|
|
|
|
子类实现抽象方法:
|
|
```csharp
|
|
class 子类名 : 抽象类名
|
|
{
|
|
public override void 方法名()
|
|
{
|
|
|
|
}
|
|
}
|
|
```
|
|
|
|
注意:
|
|
抽象方法没有方法体,结尾直接写分号 `;`
|
|
|
|
----
|
|
# 基础案例
|
|
```csharp
|
|
abstract class Animal
|
|
{
|
|
public string Name;
|
|
|
|
public Animal(string name)
|
|
{
|
|
Name = name;
|
|
}
|
|
|
|
public void Eat()
|
|
{
|
|
Console.WriteLine(Name + "正在吃东西");
|
|
}
|
|
|
|
public abstract void Speak();
|
|
}
|
|
|
|
class Dog : Animal
|
|
{
|
|
public Dog(string name) : base(name)
|
|
{
|
|
}
|
|
|
|
public override void Speak()
|
|
{
|
|
Console.WriteLine(Name + ":汪汪");
|
|
}
|
|
}
|
|
|
|
class Cat : Animal
|
|
{
|
|
public Cat(string name) : base(name)
|
|
{
|
|
}
|
|
|
|
public override void Speak()
|
|
{
|
|
Console.WriteLine(Name + ":喵喵");
|
|
}
|
|
}
|
|
|
|
Animal a1 = new Dog("旺财");
|
|
Animal a2 = new Cat("小花");
|
|
|
|
a1.Eat();
|
|
a1.Speak();
|
|
|
|
a2.Eat();
|
|
a2.Speak();
|
|
```
|
|
|
|
理解:
|
|
1. `Animal` 是抽象类,不能直接 `new Animal()`
|
|
2. `Eat()` 是普通方法,子类可以直接继承使用
|
|
3. `Speak()` 是抽象方法,父类只规定“动物要会叫”
|
|
4. `Dog` 和 `Cat` 必须用 `override` 实现自己的 `Speak()`
|
|
5. `Animal a1 = new Dog()` 可以配合多态使用
|
|
|
|
----
|
|
# 抽象类不能直接实例化
|
|
错误写法:
|
|
```csharp
|
|
abstract class Animal
|
|
{
|
|
public abstract void Speak();
|
|
}
|
|
|
|
Animal animal = new Animal();
|
|
```
|
|
|
|
原因:
|
|
抽象类是不完整的类型,里面可能有没实现的抽象成员
|
|
所以不能直接创建对象
|
|
|
|
正确用法:
|
|
```csharp
|
|
Animal animal = new Dog("旺财");
|
|
animal.Speak();
|
|
```
|
|
|
|
----
|
|
# 抽象方法必须在子类中实现
|
|
如果子类不是抽象类,就必须实现父类中所有抽象方法。
|
|
|
|
```csharp
|
|
abstract class Shape
|
|
{
|
|
public abstract double GetArea();
|
|
}
|
|
|
|
class Circle : Shape
|
|
{
|
|
public double Radius;
|
|
|
|
public Circle(double radius)
|
|
{
|
|
Radius = radius;
|
|
}
|
|
|
|
public override double GetArea()
|
|
{
|
|
return 3.14 * Radius * Radius;
|
|
}
|
|
}
|
|
```
|
|
|
|
错误写法:
|
|
```csharp
|
|
class Circle : Shape
|
|
{
|
|
|
|
}
|
|
```
|
|
|
|
原因:
|
|
`Circle` 是普通类,但没有实现 `Shape` 要求的 `GetArea()`
|
|
|
|
----
|
|
# 子类也可以继续抽象
|
|
如果子类暂时不想实现所有抽象方法,那么这个子类也必须声明成 `abstract`。
|
|
|
|
```csharp
|
|
abstract class Shape
|
|
{
|
|
public abstract double GetArea();
|
|
public abstract double GetLength();
|
|
}
|
|
|
|
abstract class RoundShape : Shape
|
|
{
|
|
public override double GetLength()
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// GetArea 暂时不实现,所以 RoundShape 也必须是抽象类
|
|
}
|
|
```
|
|
|
|
理解:
|
|
普通类必须把抽象成员补完整
|
|
抽象类可以继续把没补完的内容留给下一级子类
|
|
|
|
----
|
|
# 抽象类里可以有什么
|
|
抽象类不只是能写抽象方法,也可以写普通成员。
|
|
|
|
```csharp
|
|
abstract class Character
|
|
{
|
|
public string Name;
|
|
public int Hp;
|
|
|
|
public Character(string name, int hp)
|
|
{
|
|
Name = name;
|
|
Hp = hp;
|
|
}
|
|
|
|
public void TakeDamage(int damage)
|
|
{
|
|
Hp -= damage;
|
|
Console.WriteLine(Name + "受到了" + damage + "点伤害");
|
|
}
|
|
|
|
public virtual void Move()
|
|
{
|
|
Console.WriteLine(Name + "正在移动");
|
|
}
|
|
|
|
public abstract void Attack();
|
|
}
|
|
```
|
|
|
|
抽象类中可以有:
|
|
1. 成员变量
|
|
2. 成员属性
|
|
3. 构造函数
|
|
4. 普通方法
|
|
5. 虚方法
|
|
6. 抽象方法
|
|
7. 抽象属性、抽象索引器、抽象事件
|
|
|
|
一句话:
|
|
抽象类可以有“已经写好的公共功能”,也可以有“要求子类必须补上的功能”
|
|
|
|
----
|
|
# 抽象类可以有构造函数
|
|
抽象类不能直接 `new`,但它仍然可以有构造函数。
|
|
|
|
原因:
|
|
创建子类对象时,会先构造父类那一部分
|
|
|
|
```csharp
|
|
abstract class Enemy
|
|
{
|
|
public string Name;
|
|
|
|
public Enemy(string name)
|
|
{
|
|
Name = name;
|
|
Console.WriteLine("Enemy构造");
|
|
}
|
|
|
|
public abstract void Attack();
|
|
}
|
|
|
|
class Boss : Enemy
|
|
{
|
|
public Boss(string name) : base(name)
|
|
{
|
|
Console.WriteLine("Boss构造");
|
|
}
|
|
|
|
public override void Attack()
|
|
{
|
|
Console.WriteLine(Name + "发动攻击");
|
|
}
|
|
}
|
|
|
|
Boss boss = new Boss("魔王");
|
|
```
|
|
|
|
执行顺序还是:
|
|
1. 先执行父类 `Enemy` 构造函数
|
|
2. 再执行子类 `Boss` 构造函数
|
|
|
|
----
|
|
# abstract、virtual、override 的关系
|
|
`abstract` 方法:
|
|
```csharp
|
|
public abstract void Attack();
|
|
```
|
|
|
|
特点:
|
|
1. 没有方法体
|
|
2. 子类必须实现
|
|
3. 本身就表示允许被重写,所以子类用 `override`
|
|
|
|
`virtual` 方法:
|
|
```csharp
|
|
public virtual void Move()
|
|
{
|
|
Console.WriteLine("默认移动");
|
|
}
|
|
```
|
|
|
|
特点:
|
|
1. 有方法体
|
|
2. 子类可以选择重写,也可以不重写
|
|
3. 子类重写时也用 `override`
|
|
|
|
对比:
|
|
1. `abstract`:父类不给默认实现,子类必须改
|
|
2. `virtual`:父类给默认实现,子类可以改
|
|
3. `override`:子类真正改写父类的抽象方法或虚方法
|
|
|
|
----
|
|
# 抽象方法和普通方法的区别
|
|
普通方法:
|
|
```csharp
|
|
public void Eat()
|
|
{
|
|
Console.WriteLine("吃东西");
|
|
}
|
|
```
|
|
|
|
抽象方法:
|
|
```csharp
|
|
public abstract void Speak();
|
|
```
|
|
|
|
区别:
|
|
1. 普通方法有方法体,抽象方法没有方法体
|
|
2. 普通方法可以直接继承使用,抽象方法必须由子类实现
|
|
3. 普通方法可以写在普通类或抽象类里,抽象方法只能写在抽象类里
|
|
4. 抽象方法天然就是给多态准备的
|
|
|
|
----
|
|
# 抽象属性
|
|
抽象的不一定只有方法,也可以是属性。
|
|
|
|
```csharp
|
|
abstract class Weapon
|
|
{
|
|
public abstract int Damage { get; }
|
|
}
|
|
|
|
class Sword : Weapon
|
|
{
|
|
public override int Damage
|
|
{
|
|
get
|
|
{
|
|
return 10;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
简写:
|
|
```csharp
|
|
class Bow : Weapon
|
|
{
|
|
public override int Damage => 8;
|
|
}
|
|
```
|
|
|
|
理解:
|
|
父类规定“武器必须有伤害值”
|
|
不同武器自己决定伤害值是多少
|
|
|
|
----
|
|
# 抽象类和密封类不能同时用
|
|
错误写法:
|
|
```csharp
|
|
sealed abstract class Animal
|
|
{
|
|
|
|
}
|
|
```
|
|
|
|
原因:
|
|
1. `abstract` 表示这个类必须被继承后才能补完整
|
|
2. `sealed` 表示这个类不能被继承
|
|
3. 两个要求互相矛盾
|
|
|
|
这个点可以和[[密封类]]一起记。
|
|
|
|
----
|
|
# 常见错误
|
|
错误1:抽象方法写了方法体
|
|
```csharp
|
|
abstract class Animal
|
|
{
|
|
public abstract void Speak()
|
|
{
|
|
Console.WriteLine("动物叫");
|
|
}
|
|
}
|
|
```
|
|
|
|
抽象方法不能有方法体。
|
|
|
|
----
|
|
错误2:普通类里写抽象方法
|
|
```csharp
|
|
class Animal
|
|
{
|
|
public abstract void Speak();
|
|
}
|
|
```
|
|
|
|
抽象方法只能写在抽象类中。
|
|
|
|
----
|
|
错误3:子类没实现抽象方法
|
|
```csharp
|
|
abstract class Animal
|
|
{
|
|
public abstract void Speak();
|
|
}
|
|
|
|
class Dog : Animal
|
|
{
|
|
|
|
}
|
|
```
|
|
|
|
`Dog` 如果是普通类,就必须 `override Speak()`。
|
|
|
|
----
|
|
错误4:抽象方法上再写 `virtual`
|
|
```csharp
|
|
abstract class Animal
|
|
{
|
|
public abstract virtual void Speak();
|
|
}
|
|
```
|
|
|
|
抽象方法本身就表示必须被重写,不需要再写 `virtual`。
|
|
|
|
----
|
|
错误5:直接 new 抽象类
|
|
```csharp
|
|
Animal animal = new Animal();
|
|
```
|
|
|
|
抽象类不能直接实例化。
|
|
|
|
----
|
|
# 什么时候用抽象类
|
|
适合用抽象类的情况:
|
|
1. 多个子类确实属于同一种父类
|
|
2. 父类有公共字段、属性、构造函数或普通方法可以复用
|
|
3. 父类还需要强制子类实现某些行为
|
|
4. 你希望调用方通过父类统一调用子类对象
|
|
|
|
例如:
|
|
- `Animal`:动物都能吃,但不同动物叫声不同
|
|
- `Shape`:图形都有面积,但不同图形面积算法不同
|
|
- `Character`:角色都有血量,但不同角色攻击方式不同
|
|
|
|
不适合用抽象类的情况:
|
|
1. 只是想规定一种能力,例如“能飞”“能攻击”
|
|
2. 多个类之间没有明确的父子关系
|
|
3. 一个类需要同时拥有多种能力
|
|
|
|
这些情况后面通常更适合用[[接口]]。
|
|
|
|
----
|
|
# 和多态的关系
|
|
抽象类经常和多态一起使用。
|
|
|
|
```csharp
|
|
abstract class Skill
|
|
{
|
|
public abstract void Release();
|
|
}
|
|
|
|
class FireSkill : Skill
|
|
{
|
|
public override void Release()
|
|
{
|
|
Console.WriteLine("释放火球");
|
|
}
|
|
}
|
|
|
|
class IceSkill : Skill
|
|
{
|
|
public override void Release()
|
|
{
|
|
Console.WriteLine("释放冰锥");
|
|
}
|
|
}
|
|
|
|
void UseSkill(Skill skill)
|
|
{
|
|
skill.Release();
|
|
}
|
|
|
|
UseSkill(new FireSkill());
|
|
UseSkill(new IceSkill());
|
|
```
|
|
|
|
理解:
|
|
1. `Skill` 规定所有技能都必须能 `Release()`
|
|
2. `FireSkill` 和 `IceSkill` 各自实现释放逻辑
|
|
3. `UseSkill` 只依赖抽象父类,不依赖具体技能类
|
|
|
|
这就是:
|
|
==抽象类定规则,多态负责运行时执行不同实现==
|
|
|
|
----
|
|
# 注意
|
|
1. 抽象类不能直接创建对象
|
|
2. 抽象方法没有方法体,结尾用分号
|
|
3. 有抽象方法的类必须是抽象类
|
|
4. 普通子类继承抽象类后,必须实现所有抽象成员
|
|
5. 抽象类可以有普通方法、字段、属性和构造函数
|
|
6. 抽象方法不能写 `private`,因为子类需要能重写它
|
|
7. 抽象方法不能同时写 `virtual`,因为它本身就是虚的
|
|
8. 抽象类不能和 `sealed` 同时使用
|
|
|
|
----
|
|
# 一句话记忆
|
|
==抽象类不能直接 new,抽象方法只定规则不写实现,子类必须 override。==
|
|
|
|
再压缩一点:
|
|
==父类抽象定规矩,子类重写补细节。==
|
|
|
|
----
|
|
# 面试/复习时怎么说
|
|
可以这样答:
|
|
|
|
“抽象类是用 `abstract` 修饰的类,不能直接实例化,主要用来作为父类。
|
|
它可以包含普通成员,也可以包含抽象成员。抽象方法没有方法体,只声明方法签名,普通子类继承后必须用 `override` 实现。
|
|
抽象类适合用来提取多个子类的共同代码,同时强制子类实现不同的行为,常和继承、多态一起使用。”
|
|
|
|
----
|
|
引用:
|
|
1. Microsoft Learn: [abstract (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/abstract)
|
|
2. Microsoft Learn: [Abstract and Sealed Classes and Class Members](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/abstract-and-sealed-classes-and-class-members)
|
|
3. Microsoft Learn: [Inheritance - derive types to create more specialized behavior](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/inheritance)
|
|
|
|
#抽象类
|
|
#抽象方法
|
|
#核心
|