最近听到有人说,根据里式替换原则,正方形不是长方形。理由如下:
class Rectangle { public: void setLength(int length) { m_nLength = length;} void setWidth(int width) { m_nWidth = width; } int area() { return m_nWidth * m_nLength; } private: int m_nLength; int m_nWidth; }; class Square : public Rectangle { public: void setLength(int length) { m_nSide = length;} void setWidth(int width) { m_nSide = width; } int area() { return m_nSide * m_nSide; } private: int m_nSide; }; void testArea(Rectangle *rec) { rec->setLength(3); rec->setWidth(4); Q_ASSERT(12 == rec->area()); }
在定义上,子类不能比父类更严格,更严格就不适用于里式替换原则了。
里式替换原则的定义如下:
Liskov于1987年提出了一个关于继承的原则“Inheritance should ensure that any property proved about supertype objects also holds for subtype objects.”——“继承必须确保超类所拥有的性质在子类中仍然成立。”也就是说,当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系。
从定义中可以看出来,的确,对于C++中的继承关系,(is-A关系),要求子类只能比父类更宽松,不能更严格。才能保证父类所有的性质在子类中都成立。
保持所有引用基类的地方必须能透明地使用其子类的对象
我们平时在数学中说的“正方形是特殊的长方形”,更精确的理解应该是“正方形属于长方形”,而不是“正方形是长方形”,当然中文中可以理解成一个意思,英文不同,is-a和beyond to是两个不同的意思。
延伸一下,“属于”可以说“正方形是长方形的”,又别扭了,因为主体变了,后者“长方形”不具备拥有属性,虽然前者具备属于属性。
有人说:为了保证里式替换原则,也就是保证继承的纯粹性,就要做到:
1:子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
2:子类中可以增加自己特有的方法。
3:当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
4:当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
第3,4条,作一特别说明:
在程序中确保调用父类的地方,换成子类后,调用的方法不变。
【虚方法当然一直会调用子类的。但是如果是子类隐藏了父类的方法呢?静态调用就会导致替换后调用的方法不一致。为了处理隐藏这种情况,就需要保证3成立,如果方法名一样,但是子类形参更宽松,就会首先匹配父类,】实际中,我并没有测到这样的情况,实际的调用是根据函数的调用主体的静态类型决定调用哪个函数,而不是严格程度!!
所以3,4就忽略吧。
程序如下:
#include <iostream> using namespace std; class Base { }; class Derived : public Base { }; class Rectangle { public: virtual void show(Base *p) { cout << "Father" << endl; } }; class Square : public Rectangle { public: //using Rectangle::show; virtual void show(Derived *p) { cout << "Son" << endl; } }; int main() { Base d; // Derived d; 两个都能调, Square *pRec = new Square; //Square *pRec = new Square; pRec->show(&d); delete pRec; /* * |----Rectangle Father *Base--| * |----Square (Father)本来应该根据类的静态类型来调用,但类型不匹配,编译不过,如果引入父类,调用的就是父类的方法 * * |-----Rectangle Father 子类并没有覆盖父类函数,根据类静态类型调用 * Derived---| * |-----Square Son * * 总之是根据函数的调用主体的静态类型决定调用哪个函数。 */ return 0; }如果子类隐藏了父类的方法,实现不一样,定然会根据对象的静态类型调用,也就定然违反了里式替换原则。
总之:里式替换原则就是要记住,多态+子类比父类更宽松(可以有自己特有的方法)
蛋疼的问题讲完,似乎饶了一大圈,有回到了原点,接下来,我来讲个古代类似的故事。
想这个问题之前,我想到了古代公孙龙说过的“白马非马”的论点。他曾经跟孔子的6世孙 孔穿 就这个问题辩论不落下风。
后来被邹衍一席话说的羞愧难当,白马非马究竟是怎样的理论,读者可以自己百度,公孙龙与邹衍的经过是怎样的呢?邹衍又说了什么呢?
齐邹衍过赵,平原君使与公孙龙论白马非马之说。邹子曰:「不可。夫辩者,别殊类使不相害,序异端使不相乱。抒意通指,明其所谓,使人与知焉,不务相迷也。故胜者不失其所守,不胜者得其所求。若是,故辩可为也。及至烦文以相假,饰辞以相敦,巧譬以相移,引人使不得及其意,如此害大道。夫缴纷争言而竞后息,不能无害君子,衍不为也。」座皆称善。公孙龙由是遂绌。
翻译如下:
邹衍路过赵国,平原君让他和公孙龙辩论“白马非马”的观点。邹衍说:“不行。所谓辩论,应该区别不同类型,不相侵害;排列不同概念,不相混淆;抒发自己的意旨和一般概念,表明自己的观点,让别人理解,而不是困惑迷惘。如此,辩论的胜者能坚持自己的立场,不胜者也能得到他所追求的真理,这样的辩论是可以进行的。如果用繁文缛节来作为凭据,用巧言饰辞来互相诋毁,用华丽词藻来从偷换概念,吸引别人使之不得要领,就会妨害治学的根本道理。那种纠缠不休,咄咄逼人,总要别人认输才肯住口的作法,有害君子风度,我邹衍是绝不参加的。”在座的人听罢都齐声叫好。从此,公孙龙便受到了冷落 。
可见,白马非马,无理取闹而已。