1. 背景
有一个功能p1,由类A完成,现在需要将功能p1进行扩展,扩展后的功能为p3,p3由原功能p1和新功能p2组成,而新功能p3和p2均由类A的子类B来完成,子类B在完成新功能p2的同时,可能会导致原有的功能p1故障。
2. 定义
所有引用基类的地方能透明的使用其子类对象进行替代。
3. 对应的解决方案
使用继承时,父类已经实现好了的方法(非抽象方法),实际上是有一系列规范和契约的,虽然不强制子类必须遵循,但如果子类对这些非抽象方法进行重写、重载后,会造成整个继承体系被破坏。而里氏替换原则也正是表达了这层含义。
4. 补充对继承的理解
继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。
5. 案例
需求一:完成两个数相减的功能,由类A完成。
1 public class A 2 { 3 /// <summary> 4 /// 两数相加 5 /// </summary> 6 /// <param name="a"></param> 7 /// <param name="b"></param> 8 /// <returns></returns> 9 public int func1(int a, int b) 10 { 11 return a - b; 12 } 13 /// <summary> 14 /// 两数相乘 15 /// </summary> 16 /// <param name="a"></param> 17 /// <param name="b"></param> 18 /// <returns></returns> 19 public int func3(int a, int b) 20 { 21 return a * b; 22 } 23 }
需求二:完成两个数相加,然后再加100的功能,由类A的子类类B完成。
1 public class B:A 2 { 3 public int func1(int a, int b) 4 { 5 return a + b; 6 } 7 public int func2(int a, int b) 8 { 9 return func1(a, b) + 100; 10 } 11 }
下面我们对上述写的代码进行测试。
(1). 用类A测试两个数相减,如:100-50
(2). 用类B测试两个数相减(因为类B继承了类A),如:100-50
(3). 用类B测试两个数相加后再加100的功能,如:100+50+100
1 public static void show() 2 { 3 //需求1:完成两个数相减的功能,由类A来完成 4 A a = new A(); 5 Console.WriteLine("100-50={0}", a.func1(100, 50)); //50 6 7 //需求2:完成两个数相加功能,然后再加100,由类A的子类B来完成 8 B b = new B(); 9 Console.WriteLine("100-50={0}", b.func1(100, 50)); //150,因为子类重写了fun1,则隐藏了父类中fun1原有的方法 (错误) 10 Console.WriteLine("100和50相加,然后再减掉100,结果为:{0}", b.func2(100, 50)); //250 11 12 }
结果:
我们会发现, 用类B测试两个数相减的功能出错了,竟然变成两数相加的功能了,原因是类B中声明了和其父类相同的方法名的方法,即隐藏了父类原有的方法,违背了里氏替换原则。
6. 里氏替换原则的深刻理解
子类可以扩展父类没有的功能,但是不能改变父类的功能
a:子类可以实现父类的抽象方法,但不能重写父类的非抽象方法。
b:子类可以增加自己特有的方法。
c:子类重载父类方法时,其方法的形参要比父类方法的形参更宽松。
d:子类实现父类抽象方法时,其方法的返回值要比父类方法的更严格。
7. 正宗的里氏替换原则的用法
public static void show() { A aa = new B(); Console.WriteLine("100*50={0}",aa.func3(100, 50)); //5000 正宗的里氏替换原则 //Console.WriteLine(aa.func2(100, 50)); //代码报错,父类不能调用子类扩展的方法 }
8. 违背里氏替换原则的后果
出错率会增高