今天有位新同事問我.Net中帶out、ref的方法簽名和普通方法簽名的有什麼區別?我覺得可以從下面的例子說明一些關鍵的地方。
一、ref/out修飾符說明
對於用ref/out修飾符的說明在MSDN上有詳細的說明,地址如下:
http://msdn.microsoft.com/en-us/library/t3c3bfhx(VS.80).aspx。
二、透過IL代碼觀察ref/out修飾的方法簽名(以值類型為例)
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查看相應的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代碼分析
某個方法被調用時會創建Evaluation Stack、局部變量區、方法參數區等存儲區被創建,具體內容可參見MSIL 心得一文。
1) Main()
.entrypoint,
當前方法為入口方法;
.maxstack 1,
將創建的Evaluation Stack元素容量最大值設置為1;
.locals init ([0] int32 p),
建立方法的“局部變量區”,該區包含一個叫p的類型為int32的局部變量;
L_0000: ldloca.s p,
從“局部變量區”取得局部變量p的內存地址並對Evaluation Stack壓棧,執行完成後的堆棧變化情況:
L_0002: call void ConsoleMain.Program::TestRef(int32&)
用call指令來調用方法,稍微說明一下call指令:
Call指令只有一個參數,就是被調用方法的標記,方法的參數會從左到右壓入”方法參數區”,對於實例方法,
其參數列表中的第一個參數是一個類型實例指針(this),它在調用方法的簽名中是不可見的但卻是第一個被壓入”方法參數區”的參數,
怎麼理解這句話呢?
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方法只有一個參數,那麼在調用Test方法前為什麼Evaluation Stack中會有兩個元素呢?實際上這個arg0就是當前實例TestClass的this指針。對於Static方法,arg0對應的將是其方法簽名中的第一個參數。
接下來才按序將方法簽名中的參數壓棧。Call指令用來調用非虛方法,雖然也可以調用虛方法,但是它不會通過實例的Vrtual table來調用,因此只會調用基類方法而不會調用子類方法。最後要說的是編譯器可以通過方法簽名來知道當前方法是實例方法還是靜態方法,因此不需要為此專門設計指令,但是通過方法簽名不能看出方法是虛的還是非虛的,所以有指令Call來調用非虛方法而由指令Callvirt來調用虛方法。回到主題:
Main方法的Evaluation Stack中的&p出棧並被壓入TestRef(int32&)方法的”方法參數區”,接下來執行TestRef(int32&)方法,由於方法無返回值,所以執行完成後Main方法的Evaluation Stack為空;從這裡也可以看出ref被編譯器編譯為&,很熟悉吧,呵呵。
L_0007: ldloc.0,
將Main方法局部變量p的值壓棧。
L_0008: call void ConsoleMain.Program::TestRef(int32),
Main方法的Evaluation Stack中的p的值出棧並被壓入TestRef(int32)方法的”方法參數區”,接下來執行TestRef(int32)方法,執行完畢後釋放相應存儲區。
L_000d: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey(),
調用mscorlib.dll中定義的ReadKey方法,調用結束後返回值類型(valuetype):System.ConsoleKeyInfo並將其壓入Main方法的Evaluation Stack。
L_0012: pop L_0013: ret,
Main方法無返回值,所以需要將其Evaluation Stack中唯一的元素出棧最後返回,Main方法執行結束,釋放相關存儲區。
2)、TestRef(ref Int32 para)
L_0000: ldarg.0,
方法參數區中第0個變量的值出棧後壓入Evaluation Stack;
L_0001: ldc.i4.2,
將int32類型的常數:2壓入Evaluation Stack;
L_0002: stind.i4,
將Evaluation Stack中的兩個元素彈出(第一個元素為一個值value,第二個元素應該是某個變量的內存地址address),最後將value存儲到address所指向的內存空間中。
L_0003: ret,方法返回,釋放相應存儲區域。
3)、TestRef(Int32 para)
L_0000:ldc.i4.1,
執行完成後的堆棧變化情況:
L_0001: starg.s para,
執行完成後的堆棧變化情況:
L_0003: ret,
方法返回,相應的Evaluation Stack、局部變量區、方法參數區等存儲區被釋放。
4)、TestRef(out Int32 para)
從方法簽名上看它只比TestRef(ref Int32 para)多一個[out],其它內容完全一樣。
在代碼中將②放開,會發現編譯不通過,說明方法簽名的區別如果僅僅是ref和out則無法實現方法的overload,也就是TestRef(ref Int32 para)和TestRef(out Int32 para)這兩個方法不能同時存在於同一個類型中。
在代碼中將①注釋而將②放開,會發現編譯不通過,因為不能將一個未初始化的變量傳給ref修飾參數的方法,但是傳給out修飾參數的方法是可以的,但是在方法返回前一定要給out修飾的參數賦值。借用MS的一句話:
the ref and out keywords are treated differently at run-time, but they are treated the same at compile time.
4、結論
(1)、有ref和out修飾參數的方法和普通方法在調用前的數據准備是不一樣的,由L_0000: ldloca.s p和L_0007: ldloc.0可以看到,前者是獲取目標變量的內存地址,後者是獲取目標變量的值,這就是所謂的傳引用和傳值。
(2)、兩個方法的區別僅僅是相同參數,一個使用的修飾符是ref,另一個是out,那麼無法重載這兩個方法,且分別編譯它們得到的IL代碼完全一樣,只是方法簽名中由out修飾的那個參數前會有個token[out]。
(3)、使用out參數的作用:我並不關心變量的初值是什麼或者我不知道初值應該賦什麼或者我只是想知道我的方法執行完成後的狀態(例如:成功or 錯誤並給出錯誤原因),因為凡是用out修飾了的參數在方法中一定要為該參數重新賦值,正如MS所說:允許方法有選擇地返回值。