Files
My-Notes/笔记/CSharp/核心/里氏替换原则.md
T
2026-06-21 09:51:55 +08:00

258 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 里氏替换原则
概念:
- 里氏替换原则(Liskov Substitution PrincipleLSP)说的是:
==所有使用父类的地方,都应该能够无障碍地替换成子类,而程序行为仍然正确==
- 简单理解:
==子类必须真的能当父类来用,而不是“语法上能继承,逻辑上却不成立”==
通常用法:
1. 判断一个继承关系到底合不合理
2. 约束子类重写父类成员时,不要破坏原有规则
3. 提醒自己:如果继承后总要“特殊处理”,那通常应该改成组合或接口
核心理解:
1. 子类可以扩展父类功能
2. 但==不能破坏父类原本承诺的行为==
3. 如果父类能做的事,子类却突然“不支持”了,那往往就违反了里氏替换原则
----
# 经典反例
错误设计:
```csharp
class Bird
{
public virtual void Fly()
{
Console.WriteLine("鸟会飞");
}
}
class Sparrow : Bird
{
public override void Fly()
{
Console.WriteLine("麻雀飞走了");
}
}
class Penguin : Bird
{
public override void Fly()
{
throw new NotSupportedException("企鹅不会飞");
}
}
void LetBirdFly(Bird bird)
{
bird.Fly();
}
LetBirdFly(new Sparrow()); // 正常
LetBirdFly(new Penguin()); // 出错
```
问题:
- `LetBirdFly(Bird bird)` 这个函数相信:只要传进来的是 `Bird`,就能 `Fly()`
-`Penguin` 继承了 `Bird` 后,却把“会飞”这个规则破坏了
- 这就说明:==`Penguin` 其实不应该直接继承这种“默认会飞”的 Bird==
----
# 更合理的改法
改法一:把“鸟”和“会飞”拆开
```csharp
abstract class Bird
{
public string Name { get; set; }
}
interface IFlyable
{
void Fly();
}
class Sparrow : Bird, IFlyable
{
public void Fly()
{
Console.WriteLine("麻雀飞走了");
}
}
class Penguin : Bird
{
public void Swim()
{
Console.WriteLine("企鹅在游泳");
}
}
void LetBirdFly(IFlyable bird)
{
bird.Fly();
}
LetBirdFly(new Sparrow()); // 正常
// LetBirdFly(new Penguin()); // 编译期就不允许,更合理
```
这样做的好处:
1. `Bird` 只表达“它是一只鸟”
2. `IFlyable` 只表达“它会飞”
3. 不会飞的鸟就不用为了继承而硬实现一个错误行为
----
# 怎么判断有没有违反
可以拿下面几个问题自检:
1. 子类是不是父类的一种?
- 如果读起来都别扭,那继承关系大概率有问题
2. 把子类对象传到父类参数的位置,原有代码还能正常工作吗?
- 如果经常要加 `if (对象是某某子类)`,设计就开始危险了
3. 子类有没有把父类原本支持的行为变成“不支持”?
- 例如:父类能 `Fly()`,子类却直接抛异常
4. 子类有没有偷偷加强限制?
- 父类允许任意正数,子类却只允许 1 到 10
5. 子类有没有把父类承诺的结果变弱?
- 父类保证成功,子类却经常返回空结果或失败
----
# 违反里氏替换原则的常见信号
1. 子类重写后直接抛 `NotSupportedException`
2. 子类必须靠很多 `if/else` 特判才能安全使用
3. 继承只是为了复用代码,但语义上并不是“is-a”关系
4. 子类把父类的规则改掉了,而不是扩展
补充:
- “能继承”不等于“该继承”
- 很多违反 LSP 的代码,表面上编译没问题,真正出问题的是业务逻辑
----
# 和继承的关系
里氏替换原则不是反对继承,而是告诉你:
- ==继承必须建立在真正的替换关系上==
- 如果子类不能稳定替换父类,就不要硬继承
- 这时通常优先考虑:
- 组合
- 接口
- 更细的抽象拆分
所以可以把它和[[继承]]一起记:
- 继承解决“复用和扩展”
- 里氏替换原则约束“这个继承是不是合理”
----
# `is` 和 `as` 关键词怎么结合理解
先说结论:
- `is``as` 是==类型判断 / 类型转换工具==
- 它们能帮你在运行时判断“这个对象现在到底是什么类型”
- 但它们==不能证明一个继承设计就合理==
## `is`
作用:
- 判断对象是否兼容某个类型
- 也常用于模式匹配
例子:
```csharp
Bird bird = new Sparrow();
if (bird is Sparrow)
{
Console.WriteLine("这是麻雀");
}
if (bird is IFlyable flyable)
{
flyable.Fly();
}
```
理解:
- `bird is Sparrow` 的意思是:当前这个对象是不是 `Sparrow`
- `bird is IFlyable flyable` 不仅判断类型,还顺手把结果保存到变量 `flyable`
## `as`
作用:
- 尝试把对象安全转换成某个引用类型或可空值类型
- 转换失败不会抛异常,而是返回 `null`
例子:
```csharp
Bird bird = new Sparrow();
IFlyable flyable = bird as IFlyable;
if (flyable != null)
{
flyable.Fly();
}
```
补充:
- `as` 常用于引用类型
- 如果转换失败,结果是 `null`
- 对普通非可空值类型不能直接用 `as`
## 它和里氏替换原则的关系
`is` / `as` 本身没有错,但如果你发现代码里总是这样:
```csharp
void Handle(Bird bird)
{
if (bird is Sparrow)
{
// 一套逻辑
}
else if (bird is Penguin)
{
// 另一套逻辑
}
}
```
那通常说明一件事:
- 你嘴上写的是“我接收 `Bird`
- 但你心里真正依赖的却是“具体子类是谁”
这往往意味着:
1. 抽象不够稳定
2. 多态没有真正用起来
3. 继承关系可能不够合理
4. 有概率正在逼着代码违反里氏替换原则
更好的方向一般是:
- 让父类 / 接口直接提供调用方真正需要的能力
- 或者把能力拆到更合适的接口里
一句话记:
- `is` / `as` 是“识别类型”的工具
- 里氏替换原则是“约束继承是否合理”的原则
- ==如果父类代码里到处都要靠 `is` / `as` 分辨子类,通常该反过来检查抽象设计==
----
# 一句话记忆
==子类可以扩展父类,但不能打破父类原来的承诺==
再压缩一点:
==父类能出现的地方,子类也必须能正常出现==
----
# 面试/复习时怎么说
可以这样答:
“里氏替换原则的意思是,子类对象必须能够替换父类对象,并且不影响程序原本的正确性。
如果一个子类继承了父类,却把父类原本支持的行为改成异常、限制更多条件,或者导致调用方需要额外特殊判断,那这个继承关系通常就不合理。
这时候应该考虑重新抽象,或者改成接口和组合。”
----
# 注意
1. 里氏替换原则是==设计原则==,不是语法规则
2. 它常和[[继承]]、[[多态vob|多态]]、[[接口]]一起理解
3. `is` / `as` 可以辅助做类型判断,但不能代替抽象设计
4. 只要你发现“某个子类总是不太像父类”,就该警惕是不是违反了 LSP
#核心
#设计原则