477 lines
7.7 KiB
Markdown
477 lines
7.7 KiB
Markdown
# 接口
|
||
概念:
|
||
接口使用 `interface` 关键字定义
|
||
它主要用来规定一组==能力 / 规则 / 契约==
|
||
|
||
简单理解:
|
||
接口不关心“你是谁”
|
||
只关心“你能不能做这件事”
|
||
|
||
例如:
|
||
`IFlyable` 表示“会飞”
|
||
不管你是鸟、飞机还是飞行道具,只要实现了这个接口,就代表你拥有飞行能力
|
||
|
||
通常用法:
|
||
1. 规定某些类必须实现哪些成员
|
||
2. 表达一种能力,例如会飞、会攻击、可保存、可移动
|
||
3. 让不同类型用同一套调用方式
|
||
4. 弥补 C# 类只能单继承的问题,因为一个类可以实现多个接口
|
||
5. 配合[[多态vob|多态]],让代码面向能力编程
|
||
|
||
----
|
||
# 语法
|
||
定义接口:
|
||
```csharp
|
||
interface 接口名
|
||
{
|
||
void 方法名();
|
||
}
|
||
```
|
||
|
||
实现接口:
|
||
```csharp
|
||
class 类名 : 接口名
|
||
{
|
||
public void 方法名()
|
||
{
|
||
|
||
}
|
||
}
|
||
```
|
||
|
||
命名习惯:
|
||
接口名通常以大写字母 `I` 开头
|
||
例如:`IFlyable`、`IAttackable`、`ISaveable`
|
||
|
||
----
|
||
# 基础案例
|
||
```csharp
|
||
interface IFlyable
|
||
{
|
||
void Fly();
|
||
}
|
||
|
||
class Bird : IFlyable
|
||
{
|
||
public void Fly()
|
||
{
|
||
Console.WriteLine("鸟在飞");
|
||
}
|
||
}
|
||
|
||
class Plane : IFlyable
|
||
{
|
||
public void Fly()
|
||
{
|
||
Console.WriteLine("飞机在飞");
|
||
}
|
||
}
|
||
|
||
void LetItFly(IFlyable flyable)
|
||
{
|
||
flyable.Fly();
|
||
}
|
||
|
||
LetItFly(new Bird());
|
||
LetItFly(new Plane());
|
||
```
|
||
|
||
理解:
|
||
1. `IFlyable` 规定实现者必须有 `Fly()` 方法
|
||
2. `Bird` 和 `Plane` 类型不同,但都实现了 `IFlyable`
|
||
3. `LetItFly` 不关心传进来的是鸟还是飞机,只关心它会不会飞
|
||
|
||
这就是接口常见用法:
|
||
==面向能力编程,而不是面向具体类型编程==
|
||
|
||
----
|
||
# 接口中的成员
|
||
常见接口成员:
|
||
```csharp
|
||
interface IMovable
|
||
{
|
||
void Move();
|
||
int Speed { get; set; }
|
||
}
|
||
```
|
||
|
||
实现:
|
||
```csharp
|
||
class Player : IMovable
|
||
{
|
||
public int Speed { get; set; }
|
||
|
||
public void Move()
|
||
{
|
||
Console.WriteLine("玩家移动");
|
||
}
|
||
}
|
||
```
|
||
|
||
接口中常见成员包括:
|
||
1. 方法
|
||
2. 属性
|
||
3. 索引器
|
||
4. 事件
|
||
|
||
初学阶段重点先掌握:
|
||
接口方法 + 接口属性
|
||
|
||
----
|
||
# 接口成员默认是 public
|
||
接口里这样写:
|
||
```csharp
|
||
interface IAttackable
|
||
{
|
||
void Attack();
|
||
}
|
||
```
|
||
|
||
实现时必须写 `public`:
|
||
```csharp
|
||
class Enemy : IAttackable
|
||
{
|
||
public void Attack()
|
||
{
|
||
Console.WriteLine("敌人攻击");
|
||
}
|
||
}
|
||
```
|
||
|
||
错误写法:
|
||
```csharp
|
||
class Enemy : IAttackable
|
||
{
|
||
void Attack()
|
||
{
|
||
}
|
||
}
|
||
```
|
||
|
||
原因:
|
||
接口成员代表对外公开的契约
|
||
类实现接口时,成员访问级别不能比接口更低
|
||
|
||
----
|
||
# 一个类可以实现多个接口
|
||
C# 的类只能继承一个父类,但可以实现多个接口。
|
||
|
||
```csharp
|
||
interface IMovable
|
||
{
|
||
void Move();
|
||
}
|
||
|
||
interface IAttackable
|
||
{
|
||
void Attack();
|
||
}
|
||
|
||
class Player : IMovable, IAttackable
|
||
{
|
||
public void Move()
|
||
{
|
||
Console.WriteLine("玩家移动");
|
||
}
|
||
|
||
public void Attack()
|
||
{
|
||
Console.WriteLine("玩家攻击");
|
||
}
|
||
}
|
||
```
|
||
|
||
理解:
|
||
`Player` 同时拥有“可移动”和“可攻击”两种能力
|
||
|
||
----
|
||
# 继承类和实现接口同时存在
|
||
写法:
|
||
```csharp
|
||
class 子类名 : 父类名, 接口1, 接口2
|
||
{
|
||
|
||
}
|
||
```
|
||
|
||
注意顺序:
|
||
==先写父类,再写接口==
|
||
|
||
案例:
|
||
```csharp
|
||
class Character
|
||
{
|
||
public string Name;
|
||
}
|
||
|
||
interface IAttackable
|
||
{
|
||
void Attack();
|
||
}
|
||
|
||
class Warrior : Character, IAttackable
|
||
{
|
||
public void Attack()
|
||
{
|
||
Console.WriteLine(Name + "挥剑攻击");
|
||
}
|
||
}
|
||
```
|
||
|
||
错误示意:
|
||
```csharp
|
||
// class Warrior : IAttackable, Character
|
||
// {
|
||
// }
|
||
```
|
||
|
||
如果既要继承类,又要实现接口,父类必须写在最前面。
|
||
|
||
----
|
||
# 接口也可以继承接口
|
||
接口之间可以继承,而且可以继承多个接口。
|
||
|
||
```csharp
|
||
interface IMovable
|
||
{
|
||
void Move();
|
||
}
|
||
|
||
interface IAttackable
|
||
{
|
||
void Attack();
|
||
}
|
||
|
||
interface IPlayerAction : IMovable, IAttackable
|
||
{
|
||
void UseItem();
|
||
}
|
||
```
|
||
|
||
实现 `IPlayerAction` 的类,必须实现:
|
||
1. `Move()`
|
||
2. `Attack()`
|
||
3. `UseItem()`
|
||
|
||
```csharp
|
||
class Player : IPlayerAction
|
||
{
|
||
public void Move()
|
||
{
|
||
}
|
||
|
||
public void Attack()
|
||
{
|
||
}
|
||
|
||
public void UseItem()
|
||
{
|
||
}
|
||
}
|
||
```
|
||
|
||
----
|
||
# 接口引用和多态
|
||
接口也可以当变量类型使用。
|
||
|
||
```csharp
|
||
IAttackable attacker = new Player();
|
||
attacker.Attack();
|
||
```
|
||
|
||
也可以作为方法参数:
|
||
```csharp
|
||
void DoAttack(IAttackable attacker)
|
||
{
|
||
attacker.Attack();
|
||
}
|
||
```
|
||
|
||
好处:
|
||
调用方只依赖 `IAttackable`
|
||
任何实现了这个接口的对象都能传进来
|
||
|
||
```csharp
|
||
DoAttack(new Player());
|
||
DoAttack(new Enemy());
|
||
```
|
||
|
||
这也是多态的一种表现。
|
||
|
||
----
|
||
# 接口和里氏替换原则
|
||
接口常用来拆分能力,避免不合理继承。
|
||
|
||
比如:
|
||
```csharp
|
||
abstract class Bird
|
||
{
|
||
public string Name;
|
||
}
|
||
|
||
interface IFlyable
|
||
{
|
||
void Fly();
|
||
}
|
||
|
||
class Sparrow : Bird, IFlyable
|
||
{
|
||
public void Fly()
|
||
{
|
||
Console.WriteLine("麻雀飞");
|
||
}
|
||
}
|
||
|
||
class Penguin : Bird
|
||
{
|
||
public void Swim()
|
||
{
|
||
Console.WriteLine("企鹅游泳");
|
||
}
|
||
}
|
||
```
|
||
|
||
这样:
|
||
1. `Bird` 只表达“鸟”
|
||
2. `IFlyable` 只表达“会飞”
|
||
3. 不会飞的企鹅不用硬实现 `Fly()`
|
||
|
||
这个例子可以和[[里氏替换原则]]一起理解。
|
||
|
||
----
|
||
# 显式实现接口
|
||
当两个接口有同名方法,或者你不希望方法直接暴露在类对象上,可以显式实现接口。
|
||
|
||
```csharp
|
||
interface IWalkable
|
||
{
|
||
void Move();
|
||
}
|
||
|
||
interface IFlyable
|
||
{
|
||
void Move();
|
||
}
|
||
|
||
class Bird : IWalkable, IFlyable
|
||
{
|
||
void IWalkable.Move()
|
||
{
|
||
Console.WriteLine("鸟在走");
|
||
}
|
||
|
||
void IFlyable.Move()
|
||
{
|
||
Console.WriteLine("鸟在飞");
|
||
}
|
||
}
|
||
|
||
Bird bird = new Bird();
|
||
|
||
IWalkable walkable = bird;
|
||
walkable.Move();
|
||
|
||
IFlyable flyable = bird;
|
||
flyable.Move();
|
||
```
|
||
|
||
注意:
|
||
显式实现接口时,不写 `public`
|
||
调用时通常要先把对象当成对应接口类型
|
||
|
||
初学阶段先知道有这个写法即可,最常用的还是普通实现接口。
|
||
|
||
----
|
||
# 常见错误
|
||
错误1:实现接口时忘记 `public`
|
||
```csharp
|
||
class Player : IAttackable
|
||
{
|
||
void Attack()
|
||
{
|
||
}
|
||
}
|
||
```
|
||
|
||
接口成员默认是公开契约,实现时通常要写 `public`。
|
||
|
||
----
|
||
错误2:没有实现接口所有成员
|
||
```csharp
|
||
interface IMovable
|
||
{
|
||
void Move();
|
||
int Speed { get; set; }
|
||
}
|
||
|
||
class Player : IMovable
|
||
{
|
||
public void Move()
|
||
{
|
||
}
|
||
}
|
||
```
|
||
|
||
`Speed` 没实现,所以会报错。
|
||
|
||
----
|
||
错误3:把接口当成类直接创建对象
|
||
```csharp
|
||
IFlyable flyable = new IFlyable();
|
||
```
|
||
|
||
接口只是规则,不能直接 `new`。
|
||
|
||
正确写法:
|
||
```csharp
|
||
IFlyable flyable = new Bird();
|
||
```
|
||
|
||
----
|
||
# 什么时候用接口
|
||
适合用接口的情况:
|
||
1. 你要表达的是“能力”,不是“父子身份”
|
||
2. 多个无关类型都有同一种行为
|
||
3. 一个类型需要拥有多种能力
|
||
4. 想让代码依赖规则,而不是依赖具体类
|
||
|
||
例如:
|
||
- `IFlyable`:会飞
|
||
- `IAttackable`:能攻击
|
||
- `ISaveable`:可保存
|
||
- `IDamageable`:可受伤
|
||
|
||
----
|
||
# 注意
|
||
1. 接口用 `interface` 定义
|
||
2. 接口名通常以 `I` 开头
|
||
3. 接口不能直接创建对象
|
||
4. 类和结构体都可以实现接口
|
||
5. 一个类可以实现多个接口
|
||
6. 接口成员实现时通常要写 `public`
|
||
7. 接口更适合表达“能做什么”,抽象类更适合表达“是什么”
|
||
8. 接口常和多态、里氏替换原则一起理解
|
||
|
||
----
|
||
# 一句话记忆
|
||
==接口是能力契约,谁实现它,谁就必须按规则提供这些能力。==
|
||
|
||
再压缩一点:
|
||
==接口管能不能做,不管你是谁。==
|
||
|
||
----
|
||
# 面试/复习时怎么说
|
||
可以这样答:
|
||
|
||
“接口是用 `interface` 定义的一组契约,主要用来规定实现类必须提供哪些成员。
|
||
类可以实现多个接口,所以接口常用来表达能力,比如可移动、可攻击、可保存。
|
||
调用方可以面向接口编程,只依赖能力规则,不依赖具体类,这样更利于扩展和多态。”
|
||
|
||
----
|
||
引用:
|
||
1. Microsoft Learn: [interface (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/interface)
|
||
2. Microsoft Learn: [Interfaces](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/interfaces)
|
||
|
||
#接口
|
||
#核心
|