相信大家都對面向?qū)ο蟮娜齻€特征封裝、繼承、多態(tài)很熟悉,每個人都能說上一兩句,但是大多數(shù)都僅僅是知道這些是什么,不知道 CLR 內(nèi)部是如何實現(xiàn)的,所以本篇文章主要說說多態(tài)性中的一些概念已經(jīng)內(nèi)部實現(xiàn)的機理。
一、多態(tài)的概念
首先解釋下什么叫多態(tài):同一操作作用于不同的對象,可以有不同的解釋,產(chǎn)生不同的執(zhí)行結(jié)果,這就是多態(tài)性。換句話說,實際上就是同一個類型的實例調(diào)用”相同”的方法,產(chǎn)生的結(jié)果是不同的。這里的”相同”打上雙引號是因為這里的相同的方法僅僅是看上去相同的方法,實際上它們調(diào)用的方法是不同的。
說到多態(tài),我們不能免俗的提到下面幾個概念:重載、重寫、虛方法、抽象方法以及隱藏方法。下面就來一一介紹他們的概念。
1、重載(overload): 在同一個作用域(一般指一個類)的兩個或多個方法函數(shù)名相同,參數(shù)列表不同的方法叫做重載,它們有三個特點(俗稱兩必須一可以):
- 方法名必須相同
- 參數(shù)列表必須不相同
- 返回值類型可以不相同
例如:
public void Sleep() { Console.WriteLine("Animal睡覺"); } public int Sleep(int time) { Console.WriteLine("Animal{0}點睡覺", time); return time; }
2、重寫(override):子類中為滿足自己的需要來重復(fù)定義某個方法的不同實現(xiàn),需要用 override 關(guān)鍵字,被重寫的方法必須是虛方法,用的是 virtual 關(guān)鍵字。它的特點是(三個相同):
- 相同的方法名
- 相同的參數(shù)列表
- 相同的返回值
如:父類中的定義:
public virtual void EatFood() { Console.WriteLine("Animal吃東西"); }
子類中的定義:
public override void EatFood() { Console.WriteLine("Cat吃東西"); //base.EatFood(); }
小提示:經(jīng)常有童鞋問重載和重寫的區(qū)別,而且網(wǎng)絡(luò)上把這兩個的區(qū)別作為 C# 做??嫉拿嬖囶}之一。實際上這兩個概念完全沒有關(guān)系,僅僅都帶有一個”重”字。他們沒有在一起比較的意義,僅僅分辨它們不同的定義就好了。
3、虛方法:即為基類中定義的允許在派生類中重寫的方法,使用virtual關(guān)鍵字定義。如:
public virtual void EatFood() { Console.WriteLine("Animal吃東西"); }
注意:虛方法也可以被直接調(diào)用。如:
Animal a = new Animal(); a.EatFood();
執(zhí)行輸出結(jié)果為:
Animal吃東西
4、抽象方法:在基類中定義的并且必須在派生類中重寫的方法,使用 abstract 關(guān)鍵字定義。如:
public abstract class Biology { public abstract void Live(); } public class Animal : Biology { public override void Live() { Console.WriteLine("Animal重寫的抽象方法"); //throw new NotImplementedException(); } }
注意:抽象方法只能在抽象類中定義,如果不在抽象類中定義,則會報出如下錯誤:
虛方法和抽象方法的區(qū)別是:因為抽象類無法實例化,所以抽象方法沒有辦法被調(diào)用,也就是說抽象方法永遠不可能被實現(xiàn)。
5、隱藏方法:在派生類中定義的和基類中的某個方法同名的方法,使用 new 關(guān)鍵字定義。如在基類 Animal 中有一方法 Sleep():
public void Sleep() { Console.WriteLine("Animal Sleep"); }
則在派生類 Cat 中定義隱藏方法的代碼為:
new public void Sleep() { Console.WriteLine("Cat Sleep"); }
或者為:
public new void Sleep() { Console.WriteLine("Cat Sleep"); }
注意:
- (1)隱藏方法不但可以隱藏基類中的虛方法,而且也可以隱藏基類中的非虛方法。
- (2)隱藏方法中父類的實例調(diào)用父類的方法,子類的實例調(diào)用子類的方法。
- (3)和上一條對比:重寫方法中子類的變量調(diào)用子類重寫的方法,父類的變量要看這個父類引用的是子類的實例還是本身的實例,如果引用的是父類的實例那么調(diào)用基類的方法,如果引用的是派生類的實例則調(diào)用派生類的方法。
好了,基本概念講完了,下面來看一個例子,首先我們新建幾個類:
public abstract class Biology { public abstract void Live(); } public class Animal : Biology { public override void Live() { Console.WriteLine("Animal重寫的Live"); //throw new NotImplementedException(); } public void Sleep() { Console.WriteLine("Animal Sleep"); } public int Sleep(int time) { Console.WriteLine("Animal在{0}點Sleep", time); return time; } public virtual void EatFood() { Console.WriteLine("Animal EatFood"); } } public class Cat : Animal { public override void EatFood() { Console.WriteLine("Cat EatFood"); //base.EatFood(); } new public void Sleep() { Console.WriteLine("Cat Sleep"); } //public new void Sleep() //{ // Console.WriteLine("Cat Sleep"); //} } public class Dog : Animal { public override void EatFood() { Console.WriteLine("Dog EatFood"); //base.EatFood(); } }
下面來看看需要執(zhí)行的代碼:
class Program { static void Main(string[] args) { //Animal的實例 Animal a = new Animal(); //Animal的實例,引用派生類Cat對象 Animal ac = new Cat(); //Animal的實例,引用派生類Dog對象 Animal ad = new Dog(); //Cat的實例 Cat c = new Cat(); //Dog的實例 Dog d = new Dog(); //重載 a.Sleep(); a.Sleep(23); //重寫和虛方法 a.EatFood(); ac.EatFood(); ad.EatFood(); //抽象方法 a.Live(); //隱藏方法 a.Sleep(); ac.Sleep(); c.Sleep(); Console.ReadKey(); } }
首先,我們定義了幾個我們需要使用的類的實例,需要注意的是:
- (1)Biology類是抽象類,無法實例化;
- (2)變量ac是Animal的實例,但是指向一個Cat的對象。因為Cat類型是Animal類型的派生類,所以這種轉(zhuǎn)換沒有問題。這也是多態(tài)性的重點。
下面我們來一步一步的分析:
1、
//重載 a.Sleep(); a.Sleep(23);
很明顯,Animal 的變量 a 調(diào)用的兩個 Sleep 方法是重載的方法,第一句調(diào)用的是無參數(shù)的 Sleep() 方法,第二句調(diào)用的是有一個 int 參數(shù)的 Sleep 方法。注意兩個 Sleep 方法的返回值不一樣,這也說明了重寫的三個特征中的最后一個特征——返回值可以不相同。
運行的結(jié)果如下:
Animal Sleep Animal在23點Sleep
2、
//重寫和虛方法 a.EatFood(); ac.EatFood(); ad.EatFood();
在這一段中,a、ac 以及 ad 都是 Animal 的實例,但是他們引用的對象不同,a 引用的是 Animal 對象,ac 引用的是 Cat 對象,ad 引用的是 Dog 對象,這個差別會造成執(zhí)行結(jié)果的什么差別呢,請看執(zhí)行結(jié)果:
Animal EatFood Cat EatFood Dog EatFood
第一句 Animal 實例,直接調(diào)用 Animal 的虛方法 EatFood,沒有任何問題。
在第二、三句中,雖然同樣是 Animal 的實例,但是他們分別指向 Cat 和 Dog 對象,所以調(diào)用的 Cat 類和 Dog 類中各自重寫的 EatFood 方法,就像是 Cat 實例和 Dog 實例直接調(diào)用 EatFood 方法一樣。這個也就是多態(tài)性的體現(xiàn):同一操作作用于不同的對象,可以有不同的解釋,產(chǎn)生不同的執(zhí)行結(jié)果。
3、
//抽象方法 a.Live();
這個比較簡單,就是直接重寫父類 Biology 中的 Live 方法,執(zhí)行結(jié)果如下:
Animal重寫的Live
4、
//隱藏方法 a.Sleep(); ac.Sleep(); c.Sleep();
在分析隱藏方法時要和虛方法、重寫相互比較。變量 a 調(diào)用 Animal 類的 Sleep 方法以及變量 c 調(diào)用 Cat 類的 Sleep 方法沒有異議,但是變量 ac 引用的是一個 Cat 類型的對象,它應(yīng)該調(diào)用 Animal 類型的 EatFood 方法呢,還是 Cat 類型的 EatFood 方法呢?答案是調(diào)用父類即 Animal 的 EatFood 方法。執(zhí)行結(jié)果如下:
Animal Sleep Animal Sleep Cat Sleep
大多數(shù)的文章都是介紹到這里為止,僅僅是讓我們知道這些概念以及調(diào)用的方法,而沒有說明為什么會這樣。下面我們就來深入一點,談?wù)劧鄳B(tài)背后的機理。
二、深入理解多態(tài)性
要深入理解多態(tài)性,就要先從值類型和引用類型說起。我們都知道值類型是保存在線程棧上的,而引用類型是保存在托管堆中的。因為所有的類都是引用類型,所以我們僅僅看引用類型。
現(xiàn)在回到剛才的例子,Main 函數(shù)時程序的入口,在 JIT 編譯器將 Main 函數(shù)編譯為本地CPU指定時,發(fā)現(xiàn)該方法引用了 Biology、Animal、Cat、Dog這幾個類,所以 CLR 會創(chuàng)建幾個實例來表示這幾個類型本身,我們把它稱之為”類型對象”。該對象包含了類中的靜態(tài)字段,以及包含類中所有方法的方法表,還包含了托管堆中所有對象都要有的兩個額外的成員——類型對象指針(Type Object Point)和同步塊索引(sync Block Index)。
可能上面這段對于有些沒有看過相關(guān)CLR書籍的童鞋沒有看懂,所以我們畫個圖來描述一下:
上面的這個圖是在執(zhí)行 Main 函數(shù)之前 CLR 所做的事情,下面開始執(zhí)行 Main 函數(shù)(方便起見,簡化一下 Main 函數(shù)):
//Animal的實例 Animal a = new Animal(); //Animal的實例,引用派生類Cat對象 Animal ac = new Cat(); //Animal的實例,引用派生類Dog對象 Animal ad = new Dog(); a.Sleep(); a.EatFood(); ac.EatFood(); ad.EatFood();
下面實例化三個 Animal 實例,但是他們實際上指向的分別是 Animal 對象、Cat 對象和 Dog 對象,如下圖:
請注意,變量 ac 和 ad 雖然都是 Animal 類型,但是指向的分別是 Cat 對象和 Dog 對象,這里是關(guān)鍵。
當(dāng)執(zhí)行 a.Sleep() 時,由于 Sleep 是非虛實例方法,JIT 編譯器會找到發(fā)出調(diào)用的那個變量(a)的類型(Animal)對應(yīng)的類型對象(Animal 類型對象)。然后調(diào)用該類型對象中的 Sleep 方法,如果該類型對象沒有 Sleep 方法,JIT 編譯器會回溯類的基類(一直到 Object)中查找 Sleep 方法。
當(dāng)執(zhí)行 ac.EatFood 時,由于 EatFood 是虛實例方法,JIT 編譯器調(diào)用時會在方法中生成一些額外的代碼,這些代碼會首先檢查發(fā)出調(diào)用的變量(ac),然后跟隨變量的引用地址找到發(fā)出調(diào)用的對象(Cat 對象),找到發(fā)出調(diào)用的對象對應(yīng)的類型對象(Cat 類型對象),最后在該類型對象中查找 EatFood 方法。同樣的,如果在該類型對象中沒有查找到 EatFood 方法,JIT 編譯器會回溯到該類型對象的基類中查找。
上面描述的就是 JIT 編譯器在遇到調(diào)用類型的非虛實例方法以及虛實例方法時的不同執(zhí)行方式,也這是處理這兩類方法的不同方式造成了表面上我們看到的面向?qū)ο蟮娜齻€特征之一——多態(tài)性。
原文地址:https://www.cnblogs.com/zhangkai2237/archive/2012/12/20/2826734.html