今天有位新同事问我.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(p); //③
Console.ReadKey();
}
{
para = 1;
}
{
para = 2;
}
{
Para3 = 3;
} */
}
}
{
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
}
{
.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
}
{
.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
}
{
.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
}
{
.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指令只有一个参数,就是被调用方法的标记,方法的参数会从左到右压入”方法参数区”,对于实例方法,InvokeTest方法的IL代码如下:
其参数列表中的第一个参数是一个类型实例指针(this),它在调用方法的签名中是不可见的但却是第一个被压入”方法参数区”的参数,
怎么理解这句话呢?public class TestClass
{
private void InvokeTest()
{
Test(1);
}
private void Test(Int32 i)
{
}
}.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