国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發(fā)設計 > 正文

匹夫細說C#:從園友留言到動手實現(xiàn)C#虛函數(shù)機制

2019-11-17 02:28:13
字體:
來源:轉載
供稿:網(wǎng)友

匹夫細說C#:從園友留言到動手實現(xiàn)C#虛函數(shù)機制

前言

上一篇文章匹夫通過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ù)機制。

假如只有靜態(tài)函數(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)一些什么(從最直觀的角度看)?

  • 即便將變量聲明為People,換言之變量的編譯時類型是People。但是將不同的實例引用賦值給它,它也會根據(jù)運行時類型尋找正確的被重寫的方法去調用。例如Murong中使用override關鍵字重寫的方法Introduce。
  • 將變量聲明為People,和將變量聲明為ChenJD(也就是說編譯時類型一個是People,一個是ChenJD),即使它們的運行時類型都是ChenJD,但是調用的Introduce方法顯然不同。

的確有點意思了,是嗎?

那么現(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)的呢?

從靜態(tài)方法到虛函數(shù)

其實,實現(xiàn)c#的虛函數(shù)機制只需要靜態(tài)函數(shù)和委托就夠了。所以,進入下面的內(nèi)容之前我們要先拋棄現(xiàn)有的一些現(xiàn)成的概念,比如實例函數(shù)。

此時,我們假設我們手中只有靜態(tài)函數(shù)和委托,下面匹夫就帶領各位一起去一探虛函數(shù)的究竟吧。

_this是誰很重要

虛函數(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%了。

delegate有話說

上文已經(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)    {
發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
主站蜘蛛池模板: 扶风县| 蚌埠市| 贵溪市| 阿图什市| 景宁| 措美县| 襄垣县| 横山县| 岑巩县| 遵义市| 吐鲁番市| 离岛区| 德格县| 汉中市| 马山县| 福建省| 闻喜县| 连平县| 会宁县| 孝昌县| 日喀则市| 阿拉善右旗| 柏乡县| 太原市| 清徐县| 墨玉县| 荆州市| 海宁市| 红桥区| 扎赉特旗| 吉林省| 苍梧县| 涞源县| 达拉特旗| 随州市| 龙门县| 禹城市| 舒城县| 交口县| 大安市| 中牟县|