我們創建如下的三層繼承層次類。
public abstract class Animal { public abstract void ShowType(); } public class Bird : Animal { private string type = "Bird"; public override void ShowType() { Console.WriteLine("Type is {0}", type); } } public class Chicken : Bird { private string type = "Chicken"; public override void ShowType() { Console.WriteLine("Type is {0}", type); } }
(1)簡析對象創建過程
Bird bird=new Bird();
Bird bird創建的是一個Bird類型的引用,而new Bird()完成的是創建Bird對象,分配內存空間和初始化操作,然后將這個對象引用賦給bird變量,用示例圖來表示情況就是這樣:
(2)分析CLR是如何執行對象的創建過程的?
字段是如何創建的:
我們以基類Chicken的對象創建為例,我們幻想在托管堆中,有一塊叫GC Heap的空間,用於存儲Chicken對象的字段,對象一經創建,CLR會找到其父類Bird,並為其字段在GCHeap分配了空間用於存儲,你沒聽錯,實例化Chicken對象也要將其基類的字段分配存儲空間,然后Bird類又會繼續找到其父類Animal直到System.Object,我們在各個類的字段上添加斷點進行調試,Chicken chicken=new Chicken(); 創建對象的時候,會首先進到1,依次是2、3;這也證明了實例化子類也要為父類字段分配存儲空間;
所以,對象的創建過程是按照順序完成了對整個父類及其本身字段的內存創建,並且字段的存儲順序是按照類的高低層次來的,最高層的類排在最前面,如果父類和子類出現了同名字段,則子類創建的時候,編譯器會自動加與區別這是兩個不同的字段,比如:type字段,Bird_type、Chicken_type 這樣子,到了這一步,用示例圖來表示是這樣子的:
方法表:
方法表是在什么時候創建的:
類第一次加載到AppDomain時完成的,留意到上圖中的Type_Handle沒有,在對象創建時,將附加成員Type_Handle指向方法表在LoaderHeap(加載堆)上的地址,也就是先有方法表再有對象,這樣就完成了對象與動態方法表的關聯操作。
方法表是如何生成的?
方法表的創建跟字段的創建過程類似,也是逐層遞歸知道Object類,Chicken子類生成方法列表時,先將Bird類所有虛方法復制一份,如果Chicken類有override父類的虛方法,則子類方法覆蓋相應的虛方法,同時添加子類新方法,並且順序也是父類在前,子類在后,這個順序很重要,在后面一節方法調用的時候,就會跟這個順序有關,也是很多面試題的考點。
總結:方法表 = 父類沒有被override的方法 + 子類子類的方法,所以到了這一步,用示例圖來表示應該是這樣:
Q:
好了,問題來了,那當我們以Bird bird=new Chicken(); 創建對象的時候,bird.type 等於什么?bird.ShowType()又輸出什么?本書的作者AnyTao列舉了兩個原則,當我們熟知這兩個原則和理解字段的創建、方法表的創建,就會明白為什么會出現這樣的輸出結果了。
○ 關注對象原則:Bird bird=new Chicken(); 我們關注的應該是new的是什么類,也就是關注創建的是Chicken類型的對象;
○ 執行就近原則:首先訪問離它創建最近的字段或者方法;
書中關於這段輸出結果的原因和結果篇章比較少,我不知道為什么AnyTao沒有繼續講解下去而是作為思考拋給了讀者,為此,我研究了一下,而且編寫DEMO輸出結果,在此我說說自己的見解,如果說得不妥請觀眾們評論矯正。
Bird bird=new Chicken();
bird.type的結果什么?
根據內存分布原則,此時棧上是Bird類型的變量引用,而托管堆上的是Chicken類型的對象,內存分布圖應該是這樣子的。
此時托管堆上字段有兩個,分別是Bird_type、Chicken_type(注意:編譯器不會重新命名,此處為了便於理解),那應該輸出哪個才對?根據執行就近原則,bird.type 其中的bird是Bird類型的變量引用,所以,首先會訪問 Bird_type="Bird";
bird.ShowType()的結果是什么?
我的理解是,此處也應該執行就近原則,但是Bird類中的ShowType()方法已經在Chicken類中被Override了,所以在內存分布圖中的方法表上,只能找到Chicken.ShowType();而在Chicken類中, private string type = "Chicken"; type字段被賦值,所以輸出結果為“Type is Chicken”
總結:
通過上面的文字,我們可以理解繼承的本質,無論把Bird.type設置為Public還是Private,父類的字段都早就已經存在子類對象所在托管堆的內存分配空間中了,只是設置為Private的時候,子類對象無法訪問而已。 短短的幾頁書,看的時間雖短,但是整理成章花費的時間卻不少,看看電腦屏幕右下角的時鍾,已經花費了兩個小時,包括作圖、寫代碼驗證書中結果、思考AnyTao給出的問題等,但是這一切我都覺得很值,從來沒有如此的深入研究.NET底層的知識,剛畢業面試的時候回答這類問題總是瞎蒙,深入原理、庖丁解牛乃程序員立足之根本!