# 里氏替换原则
概念：
- 里氏替换原则（Liskov Substitution Principle，LSP）说的是：
	==所有使用父类的地方，都应该能够无障碍地替换成子类，而程序行为仍然正确==
- 简单理解：
	==子类必须真的能当父类来用，而不是“语法上能继承，逻辑上却不成立”==

通常用法：
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

#核心
#设计原则
