相信大家都對面向對象的三個特征封裝、繼承、多態很熟悉,每個人都能說上一兩句,但是大多數都僅僅是知道這些是什么,不知道CLR內部是如何實現的,所以本篇文章主要說說多態性中的一些概念已經內部實現的機理。
一、多態的概念
首先解釋下什么叫多態:同一操作作用於不同的對象,可以有不同的解釋,產生不同的執行結果,這就是多態性。換句話說,實際上就是同一個類型的實例調用“相同”的方法,產生的結果是不同的。這里的“相同”打上雙引號是因為這里的相同的方法僅僅是看上去相同的方法,實際上它們調用的方法是不同的。
說到多態,我們不能免俗的提到下面幾個概念:重載、重寫、虛方法、抽象方法以及隱藏方法。下面就來一一介紹他們的概念。
1、
重載(overload):在同一個作用域(一般指一個類)的兩個或多個方法函數名相同,參數列表不同的方法叫做重載,它們有三個特點(俗稱兩必須一可以):
- 方法名必須相同
- 參數列表必須不相同
- 返回值類型可以不相同
如:
2、
重寫(override):子類中為滿足自己的需要來重復定義某個方法的不同實現,需要用override關鍵字,被重寫的方法必須是虛方法,用的是virtual關鍵字。它的特點是(三個相同):
public void Sleep() { Console.WriteLine("Animal睡覺"); } public int Sleep(int time) { Console.WriteLine("Animal{0}點睡覺", time); return time; }
- 相同的方法名
- 相同的參數列表
- 相同的返回值。
如:父類中的定義:
子類中的定義:
public virtual void EatFood() { Console.WriteLine("Animal吃東西"); }
子類中的定義:
public override void EatFood() { Console.WriteLine("Cat吃東西"); //base.EatFood(); }
tips:經常有童鞋問重載和重寫的區別,而且網絡上把這兩個的區別作為C#做常考的面試題之一。實際上這兩個概念完全沒有關系,僅僅都帶有一個“重”字。他們沒有在一起比較的意義,僅僅分辨它們不同的定義就好了。 |
3、
虛方法:即為基類中定義的允許在派生類中重寫的方法,使用virtual關鍵字定義。如:
public virtual void EatFood() { Console.WriteLine("Animal吃東西"); }
注意:虛方法也可以被直接調用。如:
Animal a = new Animal(); a.EatFood();
運行結果:

4、
抽象方法:在基類中定義的並且必須在派生類中重寫的方法,使用abstract關鍵字定義。如:
public abstract class Biology { public abstract void Live(); } public class Animal : Biology { public override void Live() { Console.WriteLine("Animal重寫的抽象方法"); //throw new NotImplementedException(); } }
注意:抽象方法只能在抽象類中定義,如果不在抽象類中定義,則會報出如下錯誤:

虛方法和抽象方法的區別是:因為抽象類無法實例化,所以抽象方法沒有辦法被調用,也就是說抽象方法永遠不可能被實現。 |
5、
隱藏方法:在派生類中定義的和基類中的某個方法同名的方法,使用new關鍵字定義。如在基類Animal中有一方法Sleep():
public void Sleep() { Console.WriteLine("Animal Sleep"); }
new public void Sleep() { Console.WriteLine("Cat Sleep"); }
或者為:
public new void Sleep() { Console.WriteLine("Cat Sleep"); }
(2)隱藏方法中父類的實例調用父類的方法,子類的實例調用子類的方法。
(3)和上一條對比:重寫方法中子類的變量調用子類重寫的方法,父類的變量要看這個父類引用的是子類的實例還是本身的實例,如果引用的是父類的實例那么調用基類的方法,如果引用的是派生類的實例則調用派生類的方法。
好了,基本概念講完了,下面來看一個例子,首先我們新建幾個類:
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(); } }
下面來看看需要執行的代碼:
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類型的派生類,所以這種轉換沒有問題。這也是多態性的重點。
下面我們來一步一步的分析:
(1)
//重載 a.Sleep(); a.Sleep(23);
很明顯,Animal的變量a調用的兩個Sleep方法是重載的方法,第一句調用的是無參數的Sleep()方法,第二句調用的是有一個int 參數的Sleep方法。注意兩個Sleep方法的返回值不一樣,這也說明了重寫的三個特征中的最后一個特征——返回值可以不相同。
運行的結果如下:

(2)
在這一段中,a、ac以及ad都是Animal的實例,但是他們引用的對象不同,a引用的是Animal對象,ac引用的是Cat對象,ad引用的是Dog對象,這個差別會造成執行結果的什么差別呢,請看執行結果:
//重寫和虛方法 a.EatFood(); ac.EatFood(); ad.EatFood();

第一句Animal實例,直接調用Animal的虛方法EatFood,沒有任何問題。
在第二、三句中,雖然同樣是Animal的實例,但是他們分別指向Cat和Dog對象,所以調用的Cat類和Dog類中各自重寫的EatFood方法,就像是Cat實例和Dog實例直接調用EatFood方法一樣。這個也就是多態性的體現:同一操作作用於不同的對象,可以有不同的解釋,產生不同的執行結果。
(3)
//抽象方法 a.Live();
這個比較簡單,就是直接重寫父類Biology中的Live方法,執行結果如下:
(4)
//隱藏方法 a.Sleep(); ac.Sleep(); c.Sleep();
在分析隱藏方法時要和虛方法、重寫相互比較。變量 a 調用 Animal 類的 Sleep 方法以及變量 c 調用 Cat 類的 Sleep 方法沒有異議,但是變量 ac 引用的是一個 Cat 類型的對象,它應該調用 Animal 類型的 EatFood 方法呢,還是 Cat 類型的 EatFood 方法呢?答案是調用父類即Animal的EatFood方法。執行結果如下:
大多數的文章都是介紹到這里為止,僅僅是讓我們知道這些概念以及調用的方法,而沒有說明為什么會這樣。下面我們就來深入一點,談談多態背后的機理。
二、深入理解多態性
要深入理解多態性,就要先從值類型和引用類型說起。我們都知道值類型是保存在線程棧上的,而引用類型是保存在托管堆中的。因為所有的類都是引用類型,所以我們僅僅看引用類型。
現在回到剛才的例子,Main函數時程序的入口,在JIT編譯器將Main函數編譯為本地CPU指定時,發現該方法引用了Biology、Animal、Cat、Dog這幾個類,所以CLR會創建幾個實例來表示這幾個類型本身,我們把它稱之為“類型對象”。該對象包含了類中的靜態字段,以及包含類中所有方法的方法表,還包含了托管堆中所有對象都要有的兩個額外的成員——類型對象指針(Type Object Point)和同步塊索引(sync Block Index)。
可能上面這段對於有些沒有看過相關CLR書籍的童鞋沒有看懂,所以我們畫個圖來描述一下:
上面的這個圖是在執行Main函數之前CLR所做的事情,下面開始執行Main函數(方便起見,簡化一下Main函數):
//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對象,這里是關鍵。
當執行a.Sleep()時,由於Sleep是非虛實例方法,JIT編譯器會找到發出調用的那個變量(a)的類型(Animal)對應的類型對象(Animal類型對象)。然后調用該類型對象中的Sleep方法,如果該類型對象沒有Sleep方法,JIT編譯器會回溯類的基類(一直到Object)中查找Sleep方法。
當執行ac.EatFood時,由於EatFood是虛實例方法,JIT編譯器調用時會在方法中生成一些額外的代碼,這些代碼會首先檢查發出調用的變量(ac),然后跟隨變量的引用地址找到發出調用的對象(Cat對象),找到發出調用的對象對應的類型對象(Cat類型對象),最后在該類型對象中查找EatFood方法。同樣的,如果在該類型對象中沒有查找到EatFood方法,JIT編譯器會回溯到該類型對象的基類中查找。
上面描述的就是JIT編譯器在遇到調用類型的非虛實例方法以及虛實例方法時的不同執行方式,也這是處理這兩類方法的不同方式造成了表面上我們看到的面向對象的三個特征之一——多態性。
好了,本篇博文開始回顧了一些關於多態性的基本概念,然后解釋了多態性的內部機理。內部JIT編譯器的部分基本是參照《CLR via C#》書中的第四章的內容,有這本書的同學可以回去翻翻看看。寫的不好的地方,請大家批評指正。