上一篇文章匹夫通過CIL代碼簡析了一下C#函數(shù)調用的話題。雖然點擊進來的童鞋并不如匹夫預料的那么多,但也還是有一些挺有質量的來自園友的回復。這不,就有一個園友提出了這樣一個代碼,這段代碼如果被編譯成CIL代碼的話,對虛函數(shù)的調用會使用call而非callvirt:
override string ToString(){ return Base.ToString();} 至于為何是這樣,匹夫在回復中也做了解釋,因為如果CIL使用callvirt指令,那么上面那段代碼其實相當于是這樣的:
override string ToString(){ return this.ToString();}所以如果使用callvirt的話,會產(chǎn)生無限遞歸的情況。那是為什么呢?因為callvirt主要會做兩件事,首先它會檢查實例是否為null。其次,如果實例不為空,則它會根據(jù)運行時類型尋找最恰當?shù)姆椒ㄈフ{用。當然,關于CIL代碼中的call和callvirt的討論是上一篇文章的內(nèi)容,在本篇文章,匹夫還是想就這個例子作為引子,聊一聊C#中的虛函數(shù)機制。
上一篇文章中,匹夫舉了一個使用“call”來調用對象為null的實例函數(shù)的例子,通過那個例子我們發(fā)現(xiàn)了原來實例函數(shù)需要將當前實例的引用作為參數(shù)傳入。由于是上一篇文章《用CIL寫程序:從“call vs callvirt”看方法調用》中的園友回復才讓匹夫有了寫這篇文章的想法,所以這里匹夫還使用上一篇文章中的例子,只不過把那篇文章中的CIL代碼換成C#。
現(xiàn)在假設我們有了以下3個類。
public class People{ PRivate string name = "People"; public virtual void Introduce() { string str = "我是People類,我叫" + this.name; System.Console.WriteLine(str); }}public class Murong : People{ private string name = "慕容小匹夫"; public override void Introduce() { string str = "我是Murong類,我叫" + this.name; System.Console.WriteLine(str); }}public class ChenJD : People{ private string name = "陳嘉棟"; public void Introduce() { string str = "我是ChenJD類,我叫" + this.name; System.Console.WriteLine(str); }}還記得前一篇文章中,匹夫提到過的編譯時類型和運行時類型嗎?簡單回顧一下,對編譯器來說,變量的類型就是你聲明它時的類型,也就是編譯時類型,假設為TypeA。但是,往往有這種情況,就是你實例化了另一個類型,假設為TypeB,并且將這個實例的引用賦值給了你之前聲明的那個變量。這就是說,在這段程序運行的時候,編譯階段被定義為TypeA類型的變量所指向的是一塊存儲了類型TypeB的實例的內(nèi)存。這里,TypeB便是運行時類型。搞清楚這一點,我們才能繼續(xù)下文的內(nèi)容。那就是我們聲明一個People類型的變量,然后再將它的派生類實例的引用賦值給這個變量,看看會有一些什么有趣的事情發(fā)生。
public class Test1{ static void Main() { //編譯時類型是People,運行時類型是People People person = new People(); person.Introduce(); //編譯時類型是People,運行時類型是Murong person = new Murong(); person.Introduce(); //編譯時類型是People,運行時類型是ChenJD person = new ChenJD(); person.Introduce(); //編譯時類型是ChenJD,運行時類型是ChenJD ChenJD chen = new ChenJD(); chen.Introduce(); }}這組實現(xiàn)其實在上一篇文章中,匹夫使用CIL代碼實現(xiàn)過。那么這里我們再重新用C#來做一遍。老套路,編譯運行。

這四條輸出結果,前2條都十分正常,沒有什么可奇怪的。但是在ChenJD這個類中,并沒有使用override關鍵字去重寫基類People中的虛方法Introduce,而是直接實現(xiàn)了一個自己的實例函數(shù)Introduce。
而奇怪的事情也恰恰發(fā)生在和ChenJD這個類相關的操作中,那就是當變量聲明為People類時,即使將ChenJD類的實例引用賦值給這個變量,調用Introduce方法,但輸出的卻不是ChenJD中重新定義的那個Introduce方法,反而很奇怪的調用起了基類People的Introduce方法。
與此同時,聲明為ChenJD類的變量chen,在調用Introduce方法時,的確是選擇了ChenJD類重新定義的Introduce。
那么我們能直觀的發(fā)現(xiàn)一些什么(從最直觀的角度看)?
的確有點意思了,是嗎?
那么現(xiàn)在假設我們的手中只有靜態(tài)方法,換言之上面例子中的虛方法,實例方法其實全部是靜態(tài)方法變化而來的,那么我們應該如何通過靜態(tài)方法來實現(xiàn)實例方法和虛方法的功能呢?
實例方法和靜態(tài)方法有什么不同呢?大概你會說一個目標是實例,一個目標是類。不錯,但除此之外它們還有什么本質的區(qū)別嗎?貌似沒有了。那么好,如果我們只有靜態(tài)方法,如何去實現(xiàn)一個實例方法的功能呢?不錯,把實例的引用當做這個靜態(tài)方法的一個參數(shù)。
那么我們就以上面的ChenJD類中的Introduce方法入手,使用靜態(tài)方法的形式去實現(xiàn)一個實例方法的功能。
//用靜態(tài)方法實現(xiàn)實例方法 public static void Introduce(ChenJD _this) { string str = "我是ChenJD類,我叫" + _this.name; System.Console.WriteLine(str); }那么我們該如何調用呢?
很簡單,直接調用ChenJD這個類的靜態(tài)方法Introduce,同時將它的實例引用作為參數(shù)傳入這個靜態(tài)方法。
//調用靜態(tài)函數(shù)實現(xiàn)的實例函數(shù)ChenJD chen = new ChenJD();ChenJD.Introduce(chen);
編譯運行的結果和上面調用實例函數(shù)是一樣的。
所以,實例函數(shù)的實現(xiàn),其實就是靠將當前實例的引用作為參數(shù)_this傳入一個靜態(tài)函數(shù)中,只不過這個參數(shù)_this對我們不可見罷了。
OK,可為什么匹夫你饒了一大圈聊怎么用靜態(tài)函數(shù)實現(xiàn)實例函數(shù)呢?這個和本文的主題有關系嗎?當然有,因為如果所謂的虛函數(shù)也是用靜態(tài)方法實現(xiàn)的呢?
其實,實現(xiàn)c#的虛函數(shù)機制只需要靜態(tài)函數(shù)和委托就夠了。所以,進入下面的內(nèi)容之前我們要先拋棄現(xiàn)有的一些現(xiàn)成的概念,比如實例函數(shù)。
此時,我們假設我們手中只有靜態(tài)函數(shù)和委托,下面匹夫就帶領各位一起去一探虛函數(shù)的究竟吧。
虛函數(shù)有什么特點呢?嗯~,匹夫簡單想了想,最大的特點可能就是需要具備在運行時選擇正確的重寫版本的能力。
假如沒有現(xiàn)成的虛函數(shù)的存在,那么在運行時才決定要調用哪個函數(shù)的能力,會讓你想到誰呢?
不錯,前方一大波delegate仿佛就在眼前。
但是還是要注意啊,我們現(xiàn)在沒有所謂的實例方法的存在,有的只是靜態(tài)方法。那么我們所有的虛函數(shù)和重寫方法,應該怎么表示呢?
不錯,和剛剛才說過的實例方法的實現(xiàn)方式一樣,將_this作為靜態(tài)函數(shù)不可見的第一個參數(shù)傳入。那么問題來了,_this到底應該是什么類型的呢?
這為什么是一個問題呢?
因為你可以有很多派生類,派生類中又可以重寫虛函數(shù),那么這個虛函數(shù)的第一個參數(shù)_this到底是誰就很重要了。所以,第一個參數(shù)_this 就是聲明這個函數(shù)的那個類型實例。具體到剛剛的例子,聲明為People類型的變量,即便被賦值為Murong的實例引用、ChenJD的實例引用卻都是去最初聲明了虛函數(shù)Introduce的People中去分派符合的重寫方法的版本,當Murong使用了override關鍵字的時候,People能夠找到Murong的重寫版本,所以調用了Murong的重寫版本。而由于ChenJD類中并沒有重寫基類的虛方法,而是重新定義了一個自己的Introduce方法,所以People找不到符合的重寫版本,輸出的就是最初定義的Introduce。而聲明為ChenJD的變量,在調用Introduce方法時,_this已經(jīng)變成了ChenJD類,和People已經(jīng)沒有關系了。
明白了這一點,我們探索C#的虛函數(shù)機制就完成了51.23198%了。
上文已經(jīng)說了,為了實現(xiàn)虛函數(shù)能夠在運行時選擇正確重寫版本的能力,我們可以考慮使用委托。將調用函數(shù)換個思路,變成對委托的調用,這樣自然就實現(xiàn)了根據(jù)不同的情況,調用不同函數(shù)。
那么我們再來改寫一下上文中的例子。
public class People{ //新增的 public Action<People> DelegateIntroduce; public string name = "People"; public static void Introduce(People _this) { string str = "我是People類,我叫" + _this.name; System.Console.WriteLine(str); }}public class Murong : People{ public string name = "慕容小匹夫"; public static void Introduce(People _this) { string str = "我是Murong類,我叫" + _this.name; System.Console.WriteLine(str); }}public class ChenJD : People{ public string name = "陳嘉棟"; public static void Introduce(People _this) { string str = "我是ChenJD類,我叫" + _this.name; System.Console.WriteLine(str); }}到此,匹夫將之前例子中的實例函數(shù)全部替換成了靜態(tài)函數(shù),而且還新增了一個委托Action<People> DelegateIntroduce。那么現(xiàn)在我們就利用這個委托,來實現(xiàn)我們的目標,將對具體函數(shù)的調用,轉換成對委托的調用。那么首先我們顯然需要一個方法,來對各個派生類的委托字段初始化賦值。
不過在此之前,匹夫查閱資料時發(fā)現(xiàn)了很有趣的一點,那就是現(xiàn)實的C#的虛函數(shù)槽(上一篇文章中提到過這個概念)其實是在類實例分配完內(nèi)存之后,但是在實例構造器調用之前就被初始化了。所以,為了模擬這一點,我們不使用實例構造器(實例構造器主要負責實例的初始化,比如字段賦值等等),而引入一個靜態(tài)Create方法,使用new來分配內(nèi)存,之后初始化我們的DelegateIntroduce也就是委托字段,之后再做一些實例構造器做的事情。這里僅僅寫出基類People的Create方法,它的派生類類似。
//使用靜態(tài)方法創(chuàng)建實例 public static People Create() { People people = new People();//僅僅分配內(nèi)存 People.InitVirCall(people);//初始化我們的委托 //TODO //之后實例構造器要做的事情 } 之后就到了我們實現(xiàn)委托字段初始化的階段了。那么無非是將派生類各自的重寫方法賦值給委托。所以,我們在此將虛函數(shù)和使用了override關鍵字的重寫版本賦值給對應的委托,而沒有使用override關鍵字的方法則不在此列,例如ChenJD類中的Introduce方法。在此需要注意,對于派生類來說,首先要調用基類中定義的為委托字段賦值的方法,將虛函數(shù)最初的定義首先賦值給委托,這其實也就是為何當沒有正確的重寫版本時,會調用在基類中最初定義的那個方法。
//基類,也就是Introduce方法的原始定義的類。 public static void InitVirCall(People people) { people.DelegateIntroduce = People.Introduce;//保證了最原始(定義)的Introduce方法賦值給委托 }//派生類Murong,重寫了Introduce方法 public static void InitVirCall(Murong murong) { People.InitVirCall(murong);//首先保證最原始也就是定義的方法在賦值給委托。 murong.DelegateIntroduce = Murong.Introduce;//其次如果有重寫版本,再賦值給委托。如沒有重寫版本則不賦值。 }//派生類ChenJD,沒有重寫Introduce方法,而是重新定義了該方法。//因為沒有重寫基類的Introduce方法,所以不能賦值給DelegateIntroduce public static void InitVirCall(ChenJD chen) { People.InitVirCall(chen); //因為ChenJD類中的Introduce是重新定義的,所以不加到委托中。 }到此。。。我們似乎又發(fā)現(xiàn)了一個新的問題。因為會涉及到實例的字段的問題,但是傳入各個Introduce的都是People類的實例,那么字段的值就不能保證是派生類自己的了。比如編譯一下上面的代碼,輸出的其實是:

所以為了能夠匹配正確的類型,我們還需要進行一步轉化。將People轉化成對應的派生類。到此,我們就利用委托和靜態(tài)方法實現(xiàn)了虛函數(shù)的機制。代碼如下:
using System;public class Test1{ static void Main() { People person = People.Create(); person.DelegateIntroduce(person); person = Murong.Create(); person.DelegateIntroduce(person); person = ChenJD.Create(); person.DelegateIntroduce(person); ChenJD chen = ChenJD.Create(); ChenJD.Introduce(chen); }}public class People{ //新增的 public Action<People> DelegateIntroduce; public string name = "People"; public static void Introduce(People _this) { string str = "我是People類,我叫" + (_this as People).name; System.Console.WriteLine(str); } public static People Create() { People people = new People();//僅僅分配內(nèi)存 People.InitVirCall(people);//初始化我們的委托 //TODO return people; } public static void InitVirCall(People people) {
新聞熱點
疑難解答