[你必須知道的.NET]第十五回:繼承本質論
發布日期:2007.9.10 作者:Anytao
2007 Anytao.com ,原創作品,轉貼請註明作者和出處。
本文將介紹以下內容:
1. 引言
關於繼承,你是否駕熟就輕,關於繼承,你是否了如指掌。
本文不討論繼承的基本概念,我們迴歸本質,從編譯器啟動並執行角度來揭示.NET繼承中的運行本源,來發現子類對象是如何?了對父類成員與方法的繼承,以最為簡陋的樣本來揭示繼承的實質,闡述繼承機制是如何被執行的,這對於更好的理解繼承,是必要且必然的。
2. 分析
下面首先以一個簡單的動物繼承體係為例,來進行說明:
public abstract class Animal
{
public abstract void ShowType();
public void Eat()
{
Console.WriteLine("Animal always eat.");
}
}
public class Bird: Animal
{
private string type = "Bird";
public override void ShowType()
{
Console.WriteLine("Type is {0}", type);
}
private string color;
public string Color
{
get { return color; }
set { color = value; }
}
}
public class Chicken : Bird
{
private string type = "Chicken";
public override void ShowType()
{
Console.WriteLine("Type is {0}", type);
}
public void ShowColor()
{
Console.WriteLine("Color is {0}", Color);
}
}
然後,在測試類別中建立各個類對象,由於Animal為抽象類別,我們只建立Bird對象和Chicken對象。
public class TestInheritance
{
public static void Main()
{
Bird bird = new Bird();
Chicken chicken = new Chicken();
}
}
下面我們從編譯角度對這一簡單的繼承樣本進行深入分析,從而瞭解.NET內部是如何?我們強調的繼承機制。
(1)我們簡要的分析一下對象的建立過程:
Bird animal = new Bird();
Bird bird建立的是一個Bird類型的引用,而new Bird()完成的是建立Bird對象,分配記憶體空間和初始化操作,然後將這個對象賦給bird引用,也就是建立bird引用與Bird對象的關聯。
(2)我們從繼承的角度來分析在編譯器編譯期是如何執行對象的建立過程,因為繼承的本質就體現於對象的建立過程。
在此我們以Chicken對象的建立為例,首先是欄位,對象一經建立,會首先找到其父類Bird,並為其欄位分配儲存空間,而Bird也會繼續找到其父類Animal,為其分配儲存空間,依次類推直到遞迴結束,也就是完成System.Object記憶體配置為止。我們可以在編譯器中逐步執行的方法來大致瞭解其分配的過程和順序,因此,對象的建立過程是按照順序完成了對整個父類及其本身欄位的記憶體建立,並且欄位的儲存順序是由上到下排列,object類的欄位排在最前面,其原因是如果父類和子類出現了同名欄位,則在子類對象建立時,編譯器會自動認為這是兩個不同的欄位而加以區別。
然後,是方法表的建立,必須明確的一點是方法表的建立是類第一次載入到CLR時完成的,在對象建立時只是將其附加成員TypeHandle指向方法列表在Loader Heap上的地址,將對象與其動態方法列表相關聯起來,因此方法表是先於對象而存在的。類似於欄位的建立過程,方法表的建立也是父類在先子類在後,原因是顯而易見的,類Chicken產生方法列表時,首先將Bird的所有方法拷貝一份,然後和Chicken本身的方法列表做以對比,如果有覆寫的虛方法則以子類方法覆蓋同名的父類方法,同時添加子類的新方法,從而建立完成Chicken的方法列表。這種建立過程也是逐層遞迴到Object類,並且方法列表中也是按照順序排列的,父類在前子類在後,其原因和欄位大同小異,留待讀者自己體味。
結合我們的分析過程,現在將對象建立的過程以簡單的圖例來揭示其在記憶體中的分配情形,如下:
從我們的分析,和上面的對象建立過程可見,對繼承的本質我們有了更明確的認識,對於以下的問題就有了清晰明白的答案:
- 繼承是可傳遞的,子類是對父類的擴充,必須繼承父類方法,同時可以添加新方法。
- 子類可以調用父類方法和欄位,而父類不能調用子類方法和欄位。
- 虛方法如何?覆寫操作,使得父類指標可以指向子類對象成員。
- new關鍵字在虛方法繼承中的阻斷作用。
你是否已經找到了理解繼承、理解動態編譯的不二法門。
3. 思考
通過上面的講述與分析,我們基本上對.NET在編譯期的實現原理有了大致的瞭解,但是還有以下的問題,一定會引起一定的疑惑,那就是:
Bird bird2 = new Chicken();
這種情況下,bird2.ShowType應該返回什麼值呢?而bird2.type有該是什麼值呢?有兩個原則,是.NET專門用於解決這一問題的:
- 關注對象原則:調用子類還是父類的方法,取決於建立的對象是子類對象還是父類對象,而不是它的參考型別。例如Bird bird2 = new Chicken()時,我們關注的是其建立對象為Chicken類型,因此子類將繼承父類的欄位和方法,或者覆寫父類的虛方法,而不用關注bird2的參考型別是否為Bird。參考型別不同的區別決定了不同的對象在方法表中不同的存取權限。
注意
根據關注對象原則,那麼下面的兩種情況又該如何區別呢?
Bird bird2 = new Chicken();
Chicken chicken = new Chicken();
根據我們上文的分析,bird2對象和chicken對象在記憶體布局上是一樣的,差別就在於其引用指標的類型不同:bird2為Bird類型指標,而chicken為Chicken類型指標。以方法調用為例,不同的類型指標在虛擬方法表中有不同的附加資訊作為標誌來區別其訪問的地址地區,稱為offset。不同類型的指標只能在其特定地址地區內進行執行,子類覆蓋父類時會保證其訪問地址地區的一致性,從而解決了不同的類型訪問具有不同的存取權限問題。
- 執行就近原則:對於同名欄位或者方法,編譯器是按照其順序尋找來引用的,也就是首先訪問離它建立最近的欄位或者方法,例如上例中的bird2,是Bird類型,因此會首先訪問Bird_type(注意編譯器是不會重新命名的,在此是為區分起見),如果type類型設為public,則在此將返回“Bird”值。這也就是為什麼在對象建立時必須將欄位按順序排列,而父類要先於子類編譯的原因了。
思考
1. 上面我們分析到bird2.type的值是“Bird”,那麼bird2.ShowType()會顯示什麼值呢?答案是“Type is Chicken”,根據本文上面的分析,想想到底為什嗎?
2. 關於new關鍵字在虛方法動態調用中的阻斷作用,也有了更明確的理論基礎。在子類方法中,如果標記new關鍵字,則意味著隱藏基類實現,其實就是建立了與父類同名的另一個方法,在編譯中這兩個方法處於動態方法表的不同地址位置,父類方法排在前面,子類方法排在後面。
4. 結論
在.NET中,如果建立一個類,則該類總是在繼承。這緣於.NET的物件導向特性,所有的類型都最終繼承自共同的根System.Object類。可見,繼承是.NET運行機制的基礎技術之一,一切皆為對象,一切皆於繼承。本文從基礎出發,深入本質探索本源,分析疑難比較鑒別。對於什麼是繼承這個話題,希望每個人能從中尋求自己的答案,理解繼承、關注封裝、玩轉多態是理解物件導向的起點,希望本文是這一旅程的起點。
[祝福]
僅以此篇獻給我的老師們:湯文海老師,陳樺老師。
參考文獻
(USA)Don Box, Essential .NET
(中國)蟲蟲,從編譯的角度看對象
溫故知新
[開篇有益]
[第一回:恩怨情仇:is和as]
[第二回:對抽象編程:介面和抽象類別]
[第三回:曆史糾葛:特性和屬性]
[第四回:後來居上:class和struct]
[第五回:深入淺出關鍵字---把new說透]
[第六回:深入淺出關鍵字---base和this]
[第七回:品味類型---從一般型別系統開始]
[第八回:品味類型---實值型別與參考型別(上)-記憶體有理]
[第九回:品味類型---實值型別與參考型別(中)-規則無邊]
[第十回:品味類型---實值型別與參考型別(下)-應用征途]
[第十一回:參數之惑---傳遞的藝術(上)]
[第十二回:參數之惑---傳遞的藝術(下)]
[第十三回:從Hello, world開始認識IL]
[第十四回:認識IL代碼---從開始到現在]
2007 Anytao.com
原創作品,轉貼請註明作者和出處,留此資訊。
本貼子以“現狀”提供且沒有任何擔保,同時也沒有授予任何權利。
This posting is provided "AS IS" with no warranties, and confers no rights.