引言

在这篇笔记中,我将用简单易懂的语言,通过生活中的比喻和 C# 代码示例,理解里氏替换原则(Liskov Substitution Principle, LSP)
LSP 是面向对象编程中的一个核心原则


里氏替换原则是什么?

简单来说,里氏替换原则就是:子类必须能完全替代父类,不出乱子。
换句话说,子类得像个靠谱的接班人,能干父类的活儿,不能干砸了。如果用子类替换了父类,程序还得正常跑,不能崩。


生活中的比喻

想象你有一辆自行车,平时骑着它去超市买菜,好用得很。有一天,你把自行车换成了一辆玩具车。玩具车也能“跑”(你推它就动),也能停,但它载不了你去超市。如果你指望用玩具车完成买菜的任务,那肯定完蛋。

问题在哪儿?
玩具车虽然也能算“交通工具”,但它干不了自行车的基本活儿——载人去目的地。这就违反了里氏替换原则:玩具车不能真正替代自行车。


C# 代码示例:违反 LSP 的例子

假设我们在写一个游戏,定义一个父类 Weapon(武器),有个方法 Attack() 表示攻击。子类 Sword(剑)继承它,实现正常的攻击功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Weapon
{
public virtual void Attack()
{
Console.WriteLine("武器攻击!");
}
}

public class Sword : Weapon
{
public override void Attack()
{
Console.WriteLine("剑砍击,造成 10 点伤害");
}
}

现在,我们加一个子类 WaterGun(水枪):

1
2
3
4
5
6
7
public class WaterGun : Weapon
{
public override void Attack()
{
Console.WriteLine("水枪喷水,造成 0 点伤害");
}
}

假设游戏里有个方法

1
2
3
4
public void UseWeapon(Weapon weapon)
{
weapon.Attack();
}

用 Sword 调用 UseWeapon 没问题,输出“剑砍击,造成 10 点伤害”,玩家满意。但如果换成 WaterGun,输出“水枪喷水,造成 0 点伤害”,玩家肯定不干——这武器没用啊!

哪儿错了?

WaterGun 继承了 Weapon,但它没干好“武器”该干的活(造成伤害),这就违反了 LSP。子类不能替代父类,还让程序逻辑变奇怪。

修复方法

怎么解决这个问题呢?我们可以把“武器”分得更清楚一点,比如分成 RealWeapon(真武器)和 FakeWeapon(假武器):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public abstract class RealWeapon
{
public virtual void Attack()
{
Console.WriteLine("真武器攻击,造成伤害");
}
}

public abstract class FakeWeapon
{
public virtual void Play()
{
Console.WriteLine("假武器玩耍,无伤害");
}
}

public class Sword : RealWeapon
{
public override void Attack()
{
Console.WriteLine("剑砍击,造成 10 点伤害");
}
}

public class WaterGun : FakeWeapon
{
public override void Play()
{
Console.WriteLine("水枪喷水,纯粹好玩");
}
}

这样,Sword 和 WaterGun 就有了清晰的定位,WaterGun 不会被误认为能打伤害,也不会干扰 RealWeapon 的逻辑,符合 LSP。

另一个生活比喻

再举个例子:你去餐厅点餐,菜单上有个“食物”类,点“汉堡”或“沙拉”都能吃饱。但如果菜单里混进个“石头”,点了“石头”却崩了牙,那就离谱了。石头不能代替食物,这违反了 LSP。

用代码表示就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Food
{
public virtual void Eat()
{
Console.WriteLine("吃食物,饱腹感 +10");
}
}

public class Burger : Food
{
public override void Eat()
{
Console.WriteLine("吃汉堡,饱腹感 +10");
}
}

public class Stone : Food
{
public override void Eat()
{
Console.WriteLine("吃石头,崩牙,饱腹感 -100");
}
}

Burger 能替代 Food,没问题。但 Stone 替换 Food 后,用户不仅没吃饱,还受伤了,这明显违背了 LSP

为什么 LSP 重要?

LSP 就像是保证你家里的东西都能正常用。你以为换个东西没啥,结果发现坏事了。比如用塑料袋代替垃圾桶,装垃圾肯定漏。在编程中,不遵守 LSP 会导致代码行为不一致,出了 bug 还不好找,尤其在大项目里,简直是灾难。

怎么遵守 LSP?

别让子类乱来:子类可以增强功能,但不能破坏父类的基本职责。

分清楚关系:如果子类干不了父类的活,就别硬塞到一起,重新设计类层次。

定好规矩:父类要明确自己的职责,子类必须老老实实遵守。