您真的了解類型轉(zhuǎn)換嗎?請止步,解惑!
2011-08-29 00:11 空逸云 閱讀(2507) 評(píng)論(33) 收藏 舉報(bào)不久前,因?yàn)閷︻愋娃D(zhuǎn)換CLR的底層實(shí)現(xiàn)很朦朧,萬不得已下,發(fā)了一篇博文請園里的各位同學(xué),大大解惑。
很多熱心的園友紛紛發(fā)表了自己的意見和見解,在各位童鞋的幫助下,逐漸理清了類型轉(zhuǎn)換的內(nèi)幕(也可能并不是很正確!),于是想再整理一次,歡迎大家指正,而且也延發(fā)了其他的問題,想與大家一起討論。
類型轉(zhuǎn)換的疑惑
在上個(gè)問題中,我聲明了兩個(gè)類,父類Person,子類Employee,當(dāng)我實(shí)例化一個(gè)子類實(shí)例,并將其賦給父類的一個(gè)變量時(shí),我很好奇,且不了解明明是子類的實(shí)例,結(jié)果能識(shí)別到父類的方法,也就是為什么能知道是父類調(diào)用了方法。
public class ClassConvert { public static void Main() { new ClassConvert().Run(); } Employee kinsen; object obj; Person kong; public void Run() { kinsen = new Employee("Kinsen", "Chan"); kinsen.SayHello(); kong = kinsen; kong.SayHello(); obj = kong; Console.ReadLine(); Console.WriteLine(kinsen.GetType()); Console.WriteLine(kong.GetType()); Console.WriteLine(obj.GetType()); Console.ReadLine(); } } public class Person { public string FirstName { get; set; } public string LastName { get; set; } public Person(string firstname, string lastName) { this.FirstName = firstname; this.LastName = lastName; } public void SayHello() { Console.WriteLine("Hello,Word"); } public override string ToString() { return FirstName + " " + LastName; } } public class Employee : Person { public Employee(string firstname, string lastname) : base(firstname, lastname) { } new public void SayHello() { Console.WriteLine("Hello,Word!My Name is " + base.FirstName); }
運(yùn)行結(jié)果如下:
我們知道,引用類型主要數(shù)據(jù)信息是存放在托管堆中,而這塊內(nèi)存中包含三大塊,同步快,類型句柄已經(jīng)實(shí)例信息,對于類型轉(zhuǎn)換也僅僅是一個(gè)isinst或castclass指令(相見《.Net本質(zhì)論 79頁》)。最后把塊托管堆上的地址賦給線程棧上的變量,也就是說線程棧上的內(nèi)存僅僅保存了一個(gè)指向托管堆上的實(shí)例地址,而托管堆上僅有該實(shí)例的數(shù)據(jù),已經(jīng)類型句柄等數(shù)據(jù),其中類型句柄又指向該實(shí)例的具體方法實(shí)例,其中包含了方法表等類型信息。于是最后我們看到三個(gè)變量的GetType都是第一個(gè)New出來的對象。但是為什么調(diào)用的方法輸出卻不同,父類變量能正確的調(diào)用它的方法,這又是為什么呢?
方法表與方法槽表
這其中涉及到方法表與方法槽表等方面的知識(shí),然而關(guān)于這塊內(nèi)部的實(shí)現(xiàn),MSDN卻沒有什么官方資料,僅僅只能從一些MVP和開發(fā)者的筆下了解到這塊的存在。在上篇博文中,Anders Tan大哥對這方面做了一個(gè)解釋:
方法表并不是指簡單的方法列表。它包含了很多的東西,它代表了一個(gè)class(不是class的實(shí)例對象),其中既有方法列表也有static成員等等,所以這也就解釋了為什么同一個(gè)class的實(shí)例中的static都是一樣的,因?yàn)閟tatic成員就存放在方法表中,而每個(gè)實(shí)例的type handle都指向自身class的方法表。接著方法表是一個(gè)包含很多元素的一個(gè)對象,所以在其中的方法列表也就是給了另外一個(gè)概念,就是方法槽表,在方法槽表中的方法也是按一定順序排列的。首先是父類的virtual方法,然后是自身的virtual方法,如果是override了父類的方法,那么父類的virtual方法就會(huì)被子類的方法所覆蓋(這也就解釋了在polymorphism下,向上轉(zhuǎn)型后調(diào)用virtual方法會(huì)執(zhí)行真正實(shí)例的方法),接著是實(shí)例方法和靜態(tài)方法。在方法槽后就是static成員。單是方法槽表本身并不包含其中各個(gè)方法的地址,我們知道.net程序在編譯后是IL代碼,但是在執(zhí)行的時(shí)候由JIT再編譯為本地代碼,那么在方法調(diào)用和方法體的關(guān)聯(lián)就會(huì)在執(zhí)行后發(fā)生變化,而這個(gè)變化并不在方法槽表中處理,而是交給了方法描述。
《.Net本質(zhì)論》對這塊也有詳細(xì)的描述:
方法表是一個(gè)帶有長度前綴的內(nèi)存地址數(shù)組,每個(gè)方法都有一個(gè)入口項(xiàng)。CLR方法表既包含實(shí)例來方法的入口,有包括靜態(tài)方法的入口。(《.Net本質(zhì)論》P155第一段倒數(shù)第三行,以下若無特別說明,則都摘自《.Net 本質(zhì)論》)
CLR通過方法的聲明類型的方法表路由(route)所有的方法調(diào)用。(P155第二段)
類型方法表的每個(gè)入口項(xiàng)指向一個(gè)唯一的存根例程(stub routine)。初始化時(shí),每個(gè)存根例程包含一個(gè)對于CLR的JIT編譯器的調(diào)用(它由內(nèi)部的PreStubWorker程序公開)P156第二段
在這里,我并不打算深究方法表與方法槽表,僅僅是對于它們做一個(gè)簡單的介紹,了解它們是什么,好方便我進(jìn)一步的探究。
那什么是方法槽表,本質(zhì)論中沒有對槽表做一個(gè)明確的定義,但是從描述中我們也可以“想象”出它該有的形象,方法槽表顧名思義,是一張表結(jié)構(gòu)的數(shù)據(jù)類型,可以將其想象成一排排的USB接口(槽口),槽口上插入(保存)的就是方法表上的偏移量(定位了方法表上的方法)。方法槽表上的順序首先是父類的virtual方法,然后是自身的virtual方法,再是自身的方法。
多態(tài)方法表(槽表)的內(nèi)在模式
CLR中,聲明一個(gè)方法,都會(huì)為該方法加上一個(gè)newslot標(biāo)記,表明這是一個(gè)新方法,若一個(gè)方法聲明為virtual且沒有標(biāo)明newslot標(biāo)記,那么CLR就將其看成是一個(gè)新方法,否則就看成是基類同名方法的重寫,如果標(biāo)明了newslot,那槽表上開辟多一個(gè)槽位來保存這個(gè)新方法,若虛方法沒有標(biāo)明newslot,則把方法槽表上相應(yīng)的“槽位”變成新方法的方法表偏移量,否則(沒有重寫虛方法),保存了基類方法的方法表偏移量。
具體調(diào)用
那么說了那么多,到底類型是怎么轉(zhuǎn)換的?原來,我都過多的把中心放在數(shù)據(jù)中(內(nèi)存),期望從托管堆,線程棧上找到什么。當(dāng)然,這注定失敗,我完全忽略了代碼的作用,畢竟,程序的執(zhí)行也就是逐步執(zhí)行代碼(指令/機(jī)器碼),上篇博文中qmxle童鞋提到IL的實(shí)現(xiàn),讓我醍醐灌頂,茅舍頓開。
樓主,SayHello()方法不是虛方法的話,是在編譯時(shí)綁定的。看看IL代碼就明白了:
IL_000b: newobj instance void ConsoleApplication15.Program/Employee::.ctor(string,
string)
IL_0010: stloc.0
IL_0011: ldloc.0
IL_0012: callvirt instance void ConsoleApplication15.Program/Employee::SayHello()
IL_0017: nop
IL_0018: ldloc.0
IL_0019: stloc.1
IL_001a: ldloc.1
IL_001b: callvirt instance void ConsoleApplication15.Program/Person::SayHello()
第一個(gè)SayHello()方法,綁定的是Employee類型;第二個(gè)SayHello()方法,綁定的是Person類型。
這也就符合了《.Net本質(zhì)論》中的說法。
當(dāng)從一個(gè)對象引用的類型轉(zhuǎn)換到另一個(gè)對象引用的類型時(shí),必須考慮兩個(gè)類型之間的關(guān)系。如果初始化引用的類型被認(rèn)定與新引用的類型兼容,那么,CLR所要做的轉(zhuǎn)換只是一個(gè)簡單的IA-32 mov指令。這通常出現(xiàn)于這樣的賦值情形中;當(dāng)一個(gè)派生類型的引用到一個(gè)直接或間接基類的引用,或則到一個(gè)一直兼容的接口引用。
所以引用類型之間的類型轉(zhuǎn)換并不存在什么效率消耗的問題,它們之間的效率消耗僅僅在轉(zhuǎn)換之前做一個(gè)兼容性檢查時(shí)會(huì)消耗CPU時(shí)間,而不像裝箱,拆箱那樣很大的性能消耗。對于新類型的操作,就是依靠CPU指令(代碼)來識(shí)別了。看最后生成的IL:
//省略前面... IL_000c: newobj instance void DebugTest.Employee::.ctor(string, string) IL_0011: stfld class DebugTest.Employee DebugTest.ClassConvert::kinsen IL_0016: ldarg.0 IL_0017: ldfld class DebugTest.Employee DebugTest.ClassConvert::kinsen IL_001c: callvirt instance void DebugTest.Employee::SayHello() IL_0021: nop IL_0022: ldarg.0 IL_0023: ldarg.0 IL_0024: ldfld class DebugTest.Employee DebugTest.ClassConvert::kinsen IL_0029: stfld class DebugTest.Person DebugTest.ClassConvert::kong IL_002e: ldarg.0 IL_002f: ldfld class DebugTest.Person DebugTest.ClassConvert::kong IL_0034: callvirt instance void DebugTest.Person::SayHello() //省略后面...
從上面可以看出,kinsen變量調(diào)用的是Employee的SayHello方法,而Kong變量調(diào)用的是Person的SayHello方法。這是因?yàn)?/p>
SayHello方法不是虛方法,且Employee類對SayHello方法用了new關(guān)鍵字,CLR識(shí)別了它們不是同一個(gè)方法,但是這樣,我又引發(fā)了另一個(gè)問題。
新問題!您知道嗎?
從上面的IL中,可以看到,分別調(diào)用了Employee和Person類SayHello,下面,我們把SayHello改成虛方法,并重寫。
public class Person { public string FirstName { get; set; } public string LastName { get; set; } public Person(string firstname, string lastName) { this.FirstName = firstname; this.LastName = lastName; } public virtual void SayHello() { Console.WriteLine("Hello,Word"); } public override string ToString() { return FirstName + " " + LastName; } } public class Employee : Person { public Employee(string firstname, string lastname) : base(firstname, lastname) { } override public void SayHello() { Console.WriteLine("Hello,Word!My Name is " + base.FirstName); } }
結(jié)果如下:
現(xiàn)在他們的輸出一樣了,我們再看看生成的IL。
IL_000c: newobj instance void DebugTest.Employee::.ctor(string, string) IL_0011: stfld class DebugTest.Employee DebugTest.ClassConvert::kinsen IL_0016: ldarg.0 IL_0017: ldfld class DebugTest.Employee DebugTest.ClassConvert::kinsen IL_001c: callvirt instance void DebugTest.Person::SayHello() IL_0021: nop IL_0022: ldarg.0 IL_0023: ldarg.0 IL_0024: ldfld class DebugTest.Employee DebugTest.ClassConvert::kinsen IL_0029: stfld class DebugTest.Person DebugTest.ClassConvert::kong IL_002e: ldarg.0 IL_002f: ldfld class DebugTest.Person DebugTest.ClassConvert::kong IL_0034: callvirt instance void DebugTest.Person::SayHello()
現(xiàn)在,它們調(diào)用的都是Person類的SayHello了,為什么會(huì)變成Person呢?預(yù)想中應(yīng)該是Employee類的SayHello才對,如果調(diào)用
Employee類的SayHello方法,那一切都能合理的解釋,但調(diào)用Person,程序是如何確定是Employee的SayHello方法呢?另外,我知道每個(gè)類型維護(hù)一張方法表,依稀記得一篇MSDN雜志上的文章說,如果在子類中找不到相關(guān)調(diào)用的方法,則會(huì)去父類的方法表中找,那么也就是子類和父類維護(hù)的方法表不一樣,子類的方法表中不會(huì)出現(xiàn)父類的方法?或許,在這里的調(diào)用程序如此,首先,會(huì)根據(jù)當(dāng)前實(shí)例instance找到類型句柄,定位到方法槽表,然后尋找槽表中匹配的方法,隨后調(diào)用?如果是如我猜想的這般,子類重寫的虛方法在方法槽表中的名稱還是父類方法的全名,而非子類的名稱?
尾聲
希望各位童鞋發(fā)表自己的見解,也希望各位大大能抒發(fā)所學(xué),不吝解答!對于上篇文章,很多童鞋都給出了自己的見解,給我理清概念有很大的幫助,在此很感謝大家,希望大家繼續(xù)發(fā)光發(fā)熱!:-)
出處:http://kongyiyun.cnblogs.com
本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利。


浙公網(wǎng)安備 33010602011771號(hào)