不同.NET方法簽名的區(qū)別詳解
我們將要講述的是.NET方法簽名的不同種類(lèi),希望這些差異的對(duì)比,能對(duì)大家了解.NET方法簽名有所幫助。
今天有位新同事問(wèn)我.NET中帶out、ref的方法簽名和普通方法簽名的有什么區(qū)別?我覺(jué)得可以從下面的例子說(shuō)明一些關(guān)鍵的地方。
一、ref/out修飾符說(shuō)明
對(duì)于用ref/out修飾符的說(shuō)明在MSDN上有詳細(xì)的說(shuō)明,地址如下:
http://msdn.microsoft.com/en-us/library/t3c3bfhx(VS.80).aspx。
二、透過(guò)IL代碼觀察ref/out修飾的方法簽名(以值類(lèi)型為例)
1、示例代碼:
- using System;
- namespace ConsoleMain
- {
- class Program
- {
- static void Main()
- {
- Int32 p ;
- TestRef(out p); //①
- //TestRef(ref p) //②
- TestRef(p); //③
- Console.ReadKey();
- }
- static void TestRef(Int32 para) //④
- {
- para = 1;
- }
- static void TestRef(out Int32 para) //⑤
- {
- para = 2;
- }
- /*static void TestRef(ref Int32 para) //⑥
- {
- Para3 = 3;
- } */
- }
- }
2、使用Reflector查看相應(yīng)的IL代碼如下:
(1) Main()
- .method private hidebysig static void Main() cil managed
- {
- .entrypoint
- .maxstack 1
- .locals init (
- [0] int32 p)
- L_0000: ldloca.s p
- L_0002: call void ConsoleMain.Program::TestRef(int32&)
- L_0007: ldloc.0
- L_0008: call void ConsoleMain.Program::TestRef(int32)
- L_000d: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
- L_0012: pop
- L_0013: ret
- }
(2) TestRef(ref Int32 para)
- .method private hidebysig static void TestRef(int32& para) cil managed
- {
- .maxstack 8
- L_0000: ldarg.0
- L_0001: ldc.i4.2
- L_0002: stind.i4
- L_0003: ret
- }
(3) TestRef(Int32 para)
- .method private hidebysig static void TestRef(int32 para) cil managed
- {
- .maxstack 8
- L_0000: ldc.i4.1
- L_0001: starg.s para
- L_0003: ret
- }
(4) TestRef(out Int32 para)
- .method private hidebysig static void TestRef([out] int32& para) cil managed
- {
- .maxstack 8
- L_0000: ldarg.0
- L_0001: ldc.i4.2
- L_0002: stind.i4
- L_0003: ret
- }
3、IL代碼分析
某個(gè)方法被調(diào)用時(shí)會(huì)創(chuàng)建Evaluation Stack、局部變量區(qū)、方法參數(shù)區(qū)等存儲(chǔ)區(qū)被創(chuàng)建,具體內(nèi)容可參見(jiàn)MSIL 心得一文。

1) Main()
.entrypoint,
當(dāng)前方法為入口方法;
.maxstack 1,
將創(chuàng)建的Evaluation Stack元素容量***值設(shè)置為1;
.locals init ([0] int32 p), 建立方法的“局部變量區(qū)”,該區(qū)包含一個(gè)叫p的類(lèi)型為int32的局部變量;L_0000: ldloca.s p,從“局部變量區(qū)”取得局部變量p的內(nèi)存地址并對(duì)Evaluation Stack壓棧,執(zhí)行完成后的堆棧變化情況:
L_0002: call void ConsoleMain.Program::TestRef(int32&) 用call指令來(lái)調(diào)用方法,稍微說(shuō)明一下call指令: Call指令只有一個(gè)參數(shù),就是被調(diào)用方法的標(biāo)記,方法的參數(shù)會(huì)從左到右壓入”方法參數(shù)區(qū)”,對(duì)于實(shí)例方法,其參數(shù)列表中的***個(gè)參數(shù)是一個(gè)類(lèi)型實(shí)例指針(this),它在調(diào)用方法的簽名中是不可見(jiàn)的但卻是***個(gè)被壓入”方法參數(shù)區(qū)”的參數(shù),怎么理解這句話呢?public class TestClass{ private void InvokeTest() { Test(1); } private void Test(Int32 i) { } } InvokeTest方法的IL代碼如下:
.method private hidebysig instance void InvokeTest() cil managed
{
.maxstack 8
L_0000: ldarg.0
L_0001: ldc.i4.1
L_0002: call instance void ConsoleMain.TestClass::Test(int32)
L_0007: ret
}
從這里可以看到,有代碼L_0000: ldarg.0,Test方法只有一個(gè)參數(shù),那么在調(diào)用Test方法前為什么Evaluation Stack中會(huì)有兩個(gè)元素呢?實(shí)際上這個(gè)arg0就是當(dāng)前實(shí)例TestClass的this指針。對(duì)于Static方法,arg0對(duì)應(yīng)的將是其方法簽名中的***個(gè)參數(shù)。 接下來(lái)才按序?qū)⒎椒ê灻械膮?shù)壓棧。Call指令用來(lái)調(diào)用非虛方法,雖然也可以調(diào)用虛方法,但是它不會(huì)通過(guò)實(shí)例的Vrtual table來(lái)調(diào)用,因此只會(huì)調(diào)用基類(lèi)方法而不會(huì)調(diào)用子類(lèi)方法。***要說(shuō)的是編譯器可以通過(guò)方法簽名來(lái)知道當(dāng)前方法是實(shí)例方法還是靜態(tài)方法,因此不需要為此專(zhuān)門(mén)設(shè)計(jì)指令,但是通過(guò)方法簽名不能看出方法是虛的還是非虛的,所以有指令Call來(lái)調(diào)用非虛方法而由指令Callvirt來(lái)調(diào)用虛方法。
回到主題: Main方法的Evaluation Stack中的&p出棧并被壓入TestRef(int32&)方法的”方法參數(shù)區(qū)”,接下來(lái)執(zhí)行TestRef(int32&)方法,由于方法無(wú)返回值,所以執(zhí)行完成后Main方法的Evaluation Stack為空;從這里也可以看出ref被編譯器編譯為&,很熟悉吧,呵呵。
L_0007: ldloc.0, 將Main方法局部變量p的值壓棧。
L_0008: call void ConsoleMain.Program::TestRef(int32), Main方法的Evaluation Stack中的p的值出棧并被壓入TestRef(int32)方法的”方法參數(shù)區(qū)”,接下來(lái)執(zhí)行TestRef(int32)方法,執(zhí)行完畢后釋放相應(yīng)存儲(chǔ)區(qū)。
L_000d: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey(), 調(diào)用mscorlib.dll中定義的ReadKey方法,調(diào)用結(jié)束后返回值類(lèi)型(valuetype):System.ConsoleKeyInfo并將其壓入Main方法的Evaluation Stack。L_0012: pop L_0013: ret, Main方法無(wú)返回值,所以需要將其Evaluation Stack中唯一的元素出棧***返回,Main方法執(zhí)行結(jié)束,釋放相關(guān)存儲(chǔ)區(qū)。
2)、TestRef(ref Int32 para) L_0000: ldarg.0, 方法參數(shù)區(qū)中第0個(gè)變量的值出棧后壓入Evaluation Stack;
L_0001: ldc.i4.2, 將int32類(lèi)型的常數(shù):2壓入Evaluation Stack;
L_0002: stind.i4, 將Evaluation Stack中的兩個(gè)元素彈出(***個(gè)元素為一個(gè)值value,第二個(gè)元素應(yīng)該是某個(gè)變量的內(nèi)存地址address),***將value存儲(chǔ)到address所指向的內(nèi)存空間中。L_0003: ret,方法返回,釋放相應(yīng)存儲(chǔ)區(qū)域。 3)、TestRef(Int32 para)
L_0000:ldc.i4.1,
執(zhí)行完成后的堆棧變化情況:
L_0001: starg.s para,
執(zhí)行完成后的堆棧變化情況:

L_0003: ret,
方法返回,相應(yīng)的Evaluation Stack、局部變量區(qū)、方法參數(shù)區(qū)等存儲(chǔ)區(qū)被釋放。
4)、TestRef(out Int32 para)
從方法簽名上看它只比TestRef(ref Int32 para)多一個(gè)[out],其它內(nèi)容完全一樣。
在代碼中將②放開(kāi),會(huì)發(fā)現(xiàn)編譯不通過(guò),說(shuō)明方法簽名的區(qū)別如果僅僅是ref和out則無(wú)法實(shí)現(xiàn)方法的overload,也就是TestRef(ref Int32 para)和TestRef(out Int32 para)這兩個(gè)方法不能同時(shí)存在于同一個(gè)類(lèi)型中。
在代碼中將①注釋而將②放開(kāi),會(huì)發(fā)現(xiàn)編譯不通過(guò),因?yàn)椴荒軐⒁粋€(gè)未初始化的變量傳給ref修飾參數(shù)的方法,但是傳給out修飾參數(shù)的方法是可以的,但是在方法返回前一定要給out修飾的參數(shù)賦值。借用MS的一句話:
the ref and out keywords are treated differently at run-time, but they are treated the same at compile time.
4、.NET方法簽名結(jié)論
(1)、有ref和out修飾參數(shù)的方法和普通方法在調(diào)用前的數(shù)據(jù)準(zhǔn)備是不一樣的,由L_0000: ldloca.s p和L_0007: ldloc.0可以看到,前者是獲取目標(biāo)變量的內(nèi)存地址,后者是獲取目標(biāo)變量的值,這就是所謂的傳引用和傳值。
(2)、兩個(gè)方法的區(qū)別僅僅是相同參數(shù),一個(gè)使用的修飾符是ref,另一個(gè)是out,那么無(wú)法重載這兩個(gè)方法,且分別編譯它們得到的IL代碼完全一樣,只是方法簽名中由out修飾的那個(gè)參數(shù)前會(huì)有個(gè)token[out]。
(3)、使用out參數(shù)的作用:我并不關(guān)心變量的初值是什么或者我不知道初值應(yīng)該賦什么或者我只是想知道我的方法執(zhí)行完成后的狀態(tài)(例如:成功or錯(cuò)誤并給出錯(cuò)誤原因),因?yàn)榉彩怯胦ut修飾了的參數(shù)在方法中一定要為該參數(shù)重新賦值,正如MS所說(shuō):允許方法有選擇地返回值。
原文標(biāo)題:理解.Net中帶out、ref的方法簽名和普通方法簽名的區(qū)別
鏈接:http://www.cnblogs.com/vivounicorn/archive/2009/09/17/1568242.html
【編輯推薦】