公有继承能够使指向基类的指针或引用实际上指向派生类对象。这一性质正好揭示了公有继承的目的:可替换性。所谓可替换性,是指在那些(多态地)使用了基类对象的代码中,使用派生类对象也是正确的,而且不需要修改这些代码。
公有继承的目的经常被误解,导致继承的误用。最常见的误用就是通过公有继承来重用基类中已有的代码。这种误用可能会破坏可替换性。根据Liskov替换原则,公有继承所表达的必须是“is-a”关系。很多时候我们把这种“is-a”关系理解为“是一个”,例如,因为 “B 是一个A”,所以我很自然地认为“B公有继承A”是恰当的。但是这种理解反而很容易导致不正确的继承。更准确地说,“B is-a A”应该理解为“B 可以当作A来用”,而这正好是可替换性所表达的意思。
“B可以当作A来用”跟“B是一个A”是一回事吗?不一定。举一个经常被引用的例子可以帮助我们理解这两种关系的区别,以及为什么不应该以“重用基类中已有的代码”作为公有继承的目的。假设现在要设计两个类Square和Rectangle,分别表示正方形和矩形。在数学上,一个正方形是一个矩形,而且正方形在很多性质和行为上与矩形类似,于是我们很自然地将Rectangle设计为基类,然后Square公有继承Rectangle,企图重用Rectangle类的大部分代码。在这种情况下,如果一个Rectangle类的指针实际上指向一个Square类的对象,那么按照我们的逻辑,把这个指针用在那些使用Rectangle类指针的代码中,应该也是正确的。
现在问题来了,假设我们要完成这样一个任务:给定一块矩形木板,要求将它锯成两块(只锯一次而且不准粘贴),使得其中一块是矩形,另一块是正方形。用函数Partition(Rectangle*)表示这一操作。显然,如果给定的是一块正方形木板,即向函数Partition传递一个指向Square类对象的指针,我们将无法完成这个任务。因为这个任务实际上隐含了一个前提:这块木板的长和宽必须是不相等的。这时正方形就不能当作矩形来用了,也就是说,Square公有继承Rectangle是不恰当的。如果我们一开始就考虑“B是否总是可以当作A来用”,而不是“B是否是一个A”,就可以避免这种误用了。B之所以应该公有继承A,是因为B总是可以替换A,而不是因为B有许多方法(method)与A相同。
解决上述问题的一个思路是:将正方形和矩形两者完全一致的那些性质和行为抽象出来,形成一个抽象基类AbcRect,然后Square和Rectangle分别继承AbcRect,并实现各自特有的性质和行为。这样既可以达到代码重用的目的,又可以保证诸如Partition(Rectangle*)之类的操作不会被误用在Square类上。
很多参考书说公有继承是实现代码重用的一种方法。这种说法是正确的,但是这里的重用首先是指重用那些使用了基类指针的代码;当然,这时派生类也可以重用基类中已有的代码,但这只是公有继承的一个有益的副作用。重用基类中的已有代码只是新代码调用旧代码的一种形式,而在面向对象技术出现之前,新代码调用旧代码就已经很容易实现了(例如函数调用)。而重用使用了基类指针的代码则使我们可以在旧代码中调用新代码(派生类中的代码),从而满足新的需求。因此,也可以这样说,继承不是为了重用以前的代码,而是为了使现在的代码将来能被方便地重用。
(参考《C++ Coding Standards: 101 Rules, Guidelines, and Best Practices》By Herb Sutter, Andrei Alexandrescu)