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