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)
|
|||
|
|
|
|||
|
|
#接口
|
|||
|
|
#核心
|