Effective C++ 讀書筆記 Item32 確保public繼承是"is a"的關係

C++面向對象程序設計中,最重要的規則便是:public繼承應當是"is-a"的關係。當Derived public繼承自Base時, 相當於你告訴編譯器和所有看到你代碼的人:Base是Derived的抽象,Derived就是一個Base,任何時候Derived都可以代替Base使用。

比如一個Student繼承自Person,那麼Person有什麼屬性Student也應該有,接受Person類型參數的函數也應當接受一個Student

void eat(const Person& p);
void study(const Person& p);
 
Person p; Student s;
eat(p); eat(s);
study(p); study(s);

語言的二義性

上述例子也好理解,也很符合直覺。但有時情況卻會不同,比如Penguin繼承自Bird,但企鵝不會飛:

class Bird{
public:
    vitural void fly();
};
class Penguin: public Bird{
    // fly??
};

這時你可能會困惑Penguin到底是否應該有fly()方法。但其實這個問題來源於自然語言的二義性: 嚴格地考慮,鳥會飛並不是所有鳥都會飛。我們對會飛的鳥單獨建模便是:

class Bird{...};
class FlyingBird: public Bird{
public:
    virtual void fly();
};
class Penguin: public Bird{...};

這樣當你調用penguin.fly()時便會編譯錯。當然另一種辦法是Penguin繼承自擁有fly()方法的Bird, 但Penguin::fly()中拋出異常。這兩種方式在概念是有區別的:前者是說企鵝不能飛;後者是說企鵝可以飛,但飛了會出錯。

哪種實現方式好呢?[Item 18]中提到,接口應當設計得不容易被誤用,最好將錯誤從運行時提前到編譯時。所以前者更好!

錯誤的繼承

生活的經驗給了我們關於對象繼承的直覺,然而並不一定正確。比如我們來實現一個正方形繼承自矩形:

class Rect{...};
void makeBigger(Rect& r){
    int oldHeight = r.height();
    r.setWidth(r.width()+10);
    assert(r.height() == oldHeight);
}
class Square: public Rect{...};
 
Square s;
assert(s.width() == s.height());
makeBigger(s);
assert(s.width() == s.height());

根據正方形的定義,寬高相等是任何時候都需要成立的。然而makeBigger卻破壞了正方形的屬性, 所以正方形並不是一個矩形(因爲矩形需要有這樣一個性質:增加寬度時高度不會變)。即Square繼承自Rect是錯誤的做法。 C++類的繼承比現實世界中的繼承關係更加嚴格:任何適用於父類的性質都要適用於子類!

本節我們談到的是"is-a"關係,類與類之間還有着其他類型的關係比如"has-a", "is-implemented-in-terms-of"等。這些在Item-38和Item-39中分別介紹。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章