第 35 講:面向?qū)ο缶幊蹋ㄆ撸豪^承的概念
我在這個教程里將面向?qū)ο蠓譃槿齻€大塊講解:
類的概念和成員
類的繼承和派生
接口
前面兩個部分都是類的使用語法,而第三個部分是另外一種數(shù)據(jù)類型(獨立于值類型和類的聲明模式)。我個人覺得這個順序比較好學習,所以我還是不參考書上是順序給大家介紹。只是說,大體上和書上都差不多,但有些地方我會作出順序上的調(diào)整。
我們正式進入第二個部分:繼承(Inheritance)。面向?qū)ο笥腥筇匦裕悍庋b、繼承和多態(tài)。封裝在前面已經(jīng)說過了,就是一種書寫代碼的模式,為了避免和防止外來人士使用代碼的時候故意找茬或者無意之間賦值產(chǎn)生的錯誤和無效數(shù)據(jù),導致程序運行不穩(wěn)定的處理機制。今天要說的繼承,也是一種代碼書寫的模式,但這是為了簡化很復雜的代碼。
Part 1 引例
考慮一個情況。假設(shè)我現(xiàn)在給 Person
設(shè)計了基本的數(shù)據(jù)處理過程,比如字段啊、屬性封裝之類的?,F(xiàn)在我們?yōu)榱送茝V和拓展出來一個新的數(shù)據(jù)類型,比如假設(shè)為 Student
的話,因為 Student
怎么著也是一個人吧,所以它應當包含所有的和 Person
類里包含的這些成員。
如果我們復制粘貼的話,肯定不便于我們使用代碼。因為萬一我給 Person
類添加了別的東西的話,顯然 Student
也應該包含這樣的類型,畢竟 Person
是一個表達“人”的類型,而 Student
作為表達“學生”的類型,難道學生就不是人了嗎?所以,肯定我們得給 Student
對應也添加上這樣的東西。每次增加都給對方增加一次,顯得很啰嗦以外,還很不方便。
C# 考慮到了這種處理機制,于是我們可這樣書寫代碼。
請注意第 20 行代碼。我們直接在 Student
這個類的聲明后追加了 : Person
的語法。這個語法稱為繼承。換言之,我們可以把 Student : Person
寫法讀作“Student
類走/從 Person
類派生下來”,或者“Person
類派生出 Student
類”。
雖然叫繼承,但為什么讀的時候說成是派生(Derive)呢?其實,也別太較真了,因為……繼承和派生是兩個動作相反的行為,僅此而已。派生是“從上面往下面”派生,繼承則是“上面給下面”繼承,所以方向是不一樣的。
不過,倒是去糾結(jié)這個詞的用法,還不如多學下 C# 的語法,對吧。所以別太較真這個詞的說法。
繼承意味著你可以把上面的東西拿下來直接用,而僅需要 : 類名
的語法就可以不復制粘貼一堆成員,然后直接通過 Student
的實體來獲取 Person
類里的成員。下面我們來說一下用法。
稍微提一下。前面代碼里有一個空的構(gòu)造器,啥參數(shù)沒有,也沒有執(zhí)行語句。不知道前面有一個比較知識點你還記得不:如果構(gòu)造器被你自己寫出來了之后,無參構(gòu)造器不會被編譯器自動生成;如果不寫構(gòu)造器的時候,無參構(gòu)造器是默認編譯器會自動生成的。
如果你想要構(gòu)造一個個體,但初始沒有賦任何值的時候,可使用無參構(gòu)造器。但因為構(gòu)造器本身在書寫了別的構(gòu)造器之后不會自動生成,因此我們需要手動寫出來。因此,無參構(gòu)造器寫出來的目的就是為了提供一個“實例化了,但沒有賦值具體信息”的個體。
除此之外,它還有一個作用,我們稍后做說明。
Part 2 繼承語法的用法
假設(shè),我們還是給 Person
類定了 Name
、Age
和 IsBoy
三個數(shù)據(jù)成員,并且寫了上面的繼承語法。那么,我們可以實例化(new
語句)一個 Student
的對象。
我們壓根沒有在 Student
類里寫別的東西,但 Name
屬性直接就可以用了。這就是繼承。
可以從語法和使用上感受得到,因為一個繼承語法就可以使用原本就寫好的那些數(shù)據(jù)成員,就好像是在繼承家產(chǎn)一樣。所以這種機制叫繼承。
Part 3 base
關(guān)鍵字調(diào)用基類構(gòu)造器
3-1 基本語法
那么,繼承的基本用法我們就說完了。是不是很簡單?是的,很簡單。下面我們來說幾個概念,然后為了介紹的內(nèi)容方便一點,這些概念還是需要了解一下的。
基類(Base Class)、父類(Parent Class):比如這個例子里,
Person
是給Student
提供基礎(chǔ)的數(shù)據(jù)成員信息的,所以Person
稱為基類或者叫父類;超類(Super Class):和基類還有父類完全是一個意思,只是說明的角度不一樣。超類說的是,站在
Student
角度來說,因為Person
帶有和Student
類里完全一致的數(shù)據(jù)成員信息是因為Person
給了提供數(shù)據(jù)的基本語法(繼承語法),所以Person
稱為Student
類的超類;子類(Child Class):在例子里,
Student
是從Person
類派生下來的類,所以Student
稱為子類。
掌握了這些詞語之后,我們來看 base
關(guān)鍵字。base
關(guān)鍵字用于直接獲取基類型的成員,它和 this
這個關(guān)鍵字的用法基本上是一樣的,唯一的區(qū)別就是指代的位置不同。this
專門表示“當前這個類的東西”;而 base
是表達“它的父類型里面的東西”。
舉個例子。我們之前寫構(gòu)造器的時候,因為構(gòu)造器基類型也有,而且我們在 Student
類里也寫了構(gòu)造器,但……因為構(gòu)造器的特殊性,我們不得不把構(gòu)造器的語法給寫出來,但這些構(gòu)造器里的這些賦值語句沒有必要重復書寫,因為基類型就已經(jīng)寫好了這些語句。于是,我們可以稍加改良,把 Student
類型的三個構(gòu)造器改成這樣:
是的,我們直接使用 : this(參數(shù))
一致的語法:: base(參數(shù))
來表達,我要調(diào)用的是基類型的哪個構(gòu)造器。比如 base(name)
,因為 name
只有一個參數(shù),是 string
類型的,因此編譯器會自動定位到基類型的“只有一個 string
類型就可以執(zhí)行的構(gòu)造器”上去,然后去執(zhí)行那個構(gòu)造器的代碼。
3-2 子類的構(gòu)造器繼承規(guī)則
前面我們介紹了基本寫法,通過一個冒號,后跟 base
關(guān)鍵字和一個參數(shù)表列來表達我調(diào)用什么基類型的構(gòu)造器。可問題來了,我能不能像前面那樣,不調(diào)用任何的基類型構(gòu)造器呢?
舉個例子。我現(xiàn)在就這樣書寫代碼:
請注意,此時的 Student
全部使用的是要么 this
的本類型的構(gòu)造器串聯(lián)調(diào)用,要么就是沒有 this
的獨立執(zhí)行的構(gòu)造器;還要注意,我們刪掉了無參構(gòu)造器。
如果要這樣寫代碼的話,那么編譯是不通過的。它會提示你這樣的東西:

那么,為什么會這樣?下面我們來說一下繼承的基本規(guī)則。
如果你要派生一個類出來,那么這個類必須至少有一個構(gòu)造器得調(diào)用基類型的構(gòu)造器以提供賦值。如果不提供基類型構(gòu)造器繼承的賦值的話,編譯器就會去找基類型的無參構(gòu)造器。如果基類型的無參構(gòu)造器也不存在、而且你寫的派生類型也沒有構(gòu)造器去調(diào)用了基類型的某個構(gòu)造器的話,編譯器就會對這個構(gòu)造器報錯。
這句話很長,我們慢慢來理解。我們看到,Student
類型有三個構(gòu)造器。我們按參數(shù)個數(shù)給每個構(gòu)造器編個號。比如只有一個 string
參數(shù)的構(gòu)造器叫①,兩個參數(shù)的叫②,而三個參數(shù)的叫③。
那么,我們就有如下的示意圖。

請注意我們這里的書寫。最下面的③通過 : this(name, age)
調(diào)用②;而②本身也通過 : this(name)
在調(diào)用①。因此實際上只有①此時是啥都沒有的。
按照前面的構(gòu)造器繼承規(guī)則,我們必須要滿足三點的其中一點才可以:
必須至少有一個
this
調(diào)用;必須至少有一個
base
調(diào)用;基類型必須有無參構(gòu)造器。
好家伙,①構(gòu)造器,這下三個條件一條都沒滿足。因此,編譯器根本不知道如何處理數(shù)據(jù)。因為你不使用基類型的構(gòu)造器的話,因為是從基類型派生下來的類型,因此肯定得遵循實例賦值的基本規(guī)則。要是你亂給字段屬性賦值的話,顯然繼承的效果就沒體現(xiàn)出來。
因此,編譯器會告訴你“你不能這么用”。正確做法是為①指定一個基類型構(gòu)造器的調(diào)用,或者在基類型里添加一個無參構(gòu)造器。
比如說用前者這個方案。那么你就得把構(gòu)造器①改成這樣:
加一個 : base(name)
,這樣好讓編譯器知道你是遵循了基類型的“執(zhí)行賦值 name
參數(shù)”的構(gòu)造器行為,編譯器自然就不再管你其它的、使用構(gòu)造器的行為了。
Part 4 什么是好的繼承關(guān)系?什么又是不好的?
我們講解了如何使用繼承關(guān)系,對類實現(xiàn)“有效的復制粘貼”。好吧,這么說好像不怎么合理,不過道理是這個道理。不過,繼承關(guān)系是誰都可以用的一種格式寫法,但有人就可能亂用或者濫用繼承關(guān)系,導致一些問題出現(xiàn),或者是破壞面向?qū)ο蟮幕纠砟睢O旅嫖覀儊碚f一下,什么樣是好的繼承關(guān)系,什么是不好的繼承關(guān)系。
4-1 好的繼承關(guān)系
要想繼承關(guān)系好,那么就得知道,為什么我們要使用繼承機制。繼承機制并不是因為單純“解決復制粘貼”才產(chǎn)生的,還要一點,就是為了解釋出自然語言里的“是什么”的關(guān)系。如果兩個物件有“什么是什么”的關(guān)聯(lián),寫成代碼的時候可能會使用繼承關(guān)系。比如前面這例子里的 Person
和 Student
。非常顯然,“學生”它就是一個“人”。這剛好符合我們說的“是什么”的關(guān)系。
如果我們能夠找到自然界這樣的“是什么”的關(guān)系的話,我們使用繼承機制實現(xiàn)起來就非常合適。比如說“狗狗是一種動物”(Dog
類從 Animal
類派生);“正方形是一種形狀”(Square
類從 Shape
類派生);“數(shù)獨是一種益智游戲”(Sudoku
類從 Puzzle
類派生);“日語是一門語言”(Japanese
類從 Language
類派生)。
這里的“是”并不是說明兩個東西完全相同、一致,而是在說它們的所屬關(guān)系,更為貼切的說法是,“A
類要從 B
類派生的話,A
指代的物體必須屬于 B
指代的物體的其中一個子類型”。顯然,“整數(shù)是數(shù)字”是滿足這種說法的,但“高興就是快樂”就不滿足,因為“高興”和“快樂”是同義詞的不同表達,它們并不是所屬關(guān)系,而是對等的關(guān)系。
因此,我們要知道繼承關(guān)系的使用,必須是一種所屬的關(guān)系才可以用。在很多教程(比如《Java 核心技術(shù)》、《CLR Via C#》這樣的書籍)里,繼承被歸為一種“is a”關(guān)系。是的,這話沒有錯,但按照我們這里說的這個繼承觀念和理念的話,“is a”或許理解成“belongs to”更為貼切。
總之,好的繼承關(guān)系并非隨時隨地解決復制粘貼就可以用。
4-2 壞的繼承關(guān)系
那么,如果不是這樣的關(guān)系的話,我們使用起來會如何呢?我們舉個例子。我們單獨為“老爸”創(chuàng)建一個類,叫做 Father
類;而子類型就是“我”,假設(shè)我們叫 Son
類。那么很顯然,我是我爸的兒子,那么我就可以使用我爸給我的東西,因此我們可以構(gòu)造出這樣的繼承關(guān)系:
是的。這樣的話,Father
的東西,你在繼承后就可以隨便使用了。可是,這樣的繼承關(guān)系真的合理嗎?
雖然,“繼承”這個概念是從“父母”到“兒子”的過渡,好像確實是如此。但是,你仔細思考一下。你如果繼承了你爸的這個類,這就是在說明“你是爸爸”。合理嗎?這顯然不合理。
是,有可能你已經(jīng)當了爸爸,但這里的 Father
和 Son
并不一定是指代你這個個體。在編程世界里,“你”只是這個 Son
類型 new
出來(用 new
語句構(gòu)造出來)的一個實例(一個個體);而“你爸”也只是 Father
類的一個 new
出來的個體??赡苣銈€人是滿足當爸爸的條件,但在抽象的編程世界里,并非所有的 Son
類型的個體都當爸爸了(ta 可能還小,還在幼兒園;也可能讀大學,但因為人太帥,要求太高導致沒有找到男/女朋友;甚至還有可能就算扯了結(jié)婚證,愛情的結(jié)晶都還沒有搞定)。
4-3 所以
所以,這里我們要講的,是要你明白,并不是任何時間、任何地點都可以用繼承,你需要看的是,這個玩意兒到底是不是真正意義上滿足、屬于它所繼承的那個類型的一個下屬子類型。而且還要看是不是充分的。必須是所有這個 A
類型都是 B
類型指代的事物的一個子類型的時候,才可使用 A
從 B
類派生的這么一個繼承關(guān)系。