用MSIL剥开C#的外衣(一):方法参数ref、out、params和lock、for和foreach关键字
上一篇 / 下一篇 2009-09-03 15:47:02 / 个人分类:C# && .NET
~-KRu"[ ^0 我们可能从来都不需要用到MSIL,但了解MSIL可以让我们了解许多其他人所不知道的内幕。本文就试图通过MSIL,剥开一些披在C#上面的漂亮外衣。 51Testing软件测试网K_5T6k*vZ7UE
对于方法参数,MSDN上这样说:“如果在为方法声明参数时未使用ref或out,则该参数可以具有关联的值。可以在方法中更改该值,但当控制传递回调用过程时,不会保留更改的值。通过使用方法参数关键字,可以更改这种行为。”这样说太抽象了,现在举一个例子来进行说明:
usingSystem;
publicclassRefOutParam
{
publicvoidNoRef(inti)
{
i = 500;
}
publicvoidTestRef(refinti)
{
i = 100;
}
publicvoidTestOut(outinti)
{
i = 200;
}
publicvoidTestParam(paramsstring[] Fields)
{
foreach(stringfieldinFields)
{
Console.WriteLine(field);
}
for(inti = 0; i < Fields.Length; i++)
Fields[i] = i.ToString();
}
publicstaticvoidMain()
{
RefOutParam TestCase =newRefOutParam();
lock(TestCase)
{
inti = 0;
TestCase.NoRef(i);
Console.WriteLine("testing no ref ...i={0}", i);
TestCase.TestRef(refi);
Console.WriteLine("testing ref.... i={0}", i);
TestCase.TestOut(outi);
Console.WriteLine("testing out.... i={0}", i);
Console.WriteLine("testing param 001");
TestCase.TestParam("001","002","003");
string[] TestParams ={"001","002","003"};
Console.WriteLine("testing param 002");
TestCase.TestParam(TestParams);
foreach(stringsinTestParams)
Console.WriteLine(s);
}
}
}
输出结果不说大家也知道,那就是:
testing no ref ...i=051Testing软件测试网H3kaR/W4jcc
testing ref.... i=10051Testing软件测试网9_$l_ Q8u}-a
testing out.... i=20051Testing软件测试网7HgJ!bW5w+`
testing param 001
?.{ f;tB6y0001
Y~CM4j/} |4L+f vO }0002
$L1?x5b5Bj$mq000351Testing软件测试网1bl8G*Z8tw;G#o"n
testing param 002
(tVQf#Z~ O0001
Tn2? x9u&Lr000251Testing软件测试网,j/e7i+Sz0kIn oK
00351Testing软件测试网\H$fcfN0z,[
051Testing软件测试网/N o1lZ7k}N'y4G,XT4e
151Testing软件测试网S;ex7FKX4]Fcd!b
2
对这些代码,我们先说说ref和out,这个已经被别人讲了许多次了,我再重复一下(领导讲话时经常这样^_^):
TestCase.NoRef(i);没有用ref/out,那么,在函数体中对参数的更改,其有效范围只在当前函数体内,出了该函数,参数的值便不再保留。
TestCase.TestRef(refi); TestCase.TestOut(outi);用了ref/out参数后,在函数体中对参数的更改,出了该函数后仍然有效。用MSDN的说法:“ref关键字使参数按引用传递。……out关键字会导致参数通过引用来传递……传递到ref参数的参数必须最先初始化。这与out不同,out的参数在传递之前不需要显式初始化……尽管作为out参数传递的变量不需要在传递之前进行初始化,但需要调用方法以便在方法返回之前赋值……ref和out关键字在运行时的处理方式不同,但在编译时的处理方式相同。”这一大段话,可以总结为“ref和out参数都是通过引用传值,ref参数在调用前必须初始化,out参数在返回前必须初始化,ref和out参数的编译处理相同,但是在运行时的处理方式不同”。通过reflector反汇编,NoRef、TestRef和TestOut的MSIL代码如下:
.methodpublichidebysiginstancevoidNoRef(int32 i)cil managed
{
.maxstack8
L_0000: nop
//把值500装入堆栈
L_0001: ldc.i4500
//把所提供的值(500)存入参数槽i所在的位置
L_0006: starg.s i
L_0008: ret
}
.methodpublichidebysiginstancevoidTestRef(int32& i)cil managed
{
.maxstack8
L_0000: nop
//把类型为int32的地址参数i装入堆栈
L_0001: ldarg.1
//把值100装入堆栈
L_0002: ldc.i4.s100
//把所提供的值(100)存入堆栈中的地址(i)
L_0004: stind.i4
L_0005: ret
}
.methodpublichidebysiginstancevoidTestOut([out] int32& i)cil managed
{
.maxstack8
L_0000: nop
//把类型为int32的地址参数i装入堆栈
L_0001: ldarg.1
//把值200装入堆栈
L_0002: ldc.i4200
//把所提供的值(200)存入堆栈中的地址(i)
L_0007: stind.i4
L_0008: ret
}
可以看出,ref和out参数都被编译成地址了。对这些参数的操作,都是在操作其地址,而不是该参数的值,所以,对这些参数的更改,实际上就是更改了相应参数的地址所指向的值。另外,在函数体的内部,对ref和out参数操作的指令是完全相同的。而没有用ref的函数,对参数的操作其实就是对参数槽的操作,并不影响到参数本身。
客户端调用的MSIL代码如下:
.localsinit (
[0]classRefOutParamparam,
[1] int32num,//int num;
……
//TestCase.TestRef(ref i);
L_002d: ldloc.0
L_002e: ldloca.s num //把num的地址装入堆栈
L_0030: callvirtinstancevoid RefOutParam::TestRef(int32&)
// TestCase.TestOut(out i);
L_0047: ldloc.0
L_0048: ldloca.s num//把num的地址装入堆栈
L_004a: callvirtinstancevoid RefOutParam::TestOut(int32&)
可以看出,客户端在调用具有ref/out参数的函数时,先取得参数的地址,然后把该地址传给被调用参数。
现在再看params、foreach和for,我们先看TestParam的MSIL代码:
.methodpublichidebysiginstancevoidTestParam(string[] Fields)cil managed
{
//变量定义
.param[1]
.custominstancevoid [mscorlib]System.ParamArrayAttribute::.ctor()
.maxstack3
.localsinit(
[0] stringtext,
[1] int32num,
[2] string[]textArray,
[3] int32num2,
[4] boolflag)
L_0000: nop
L_0001: nop
// textArray=Fields
L_0002: ldarg.1
L_0003: stloc.2
//num2=0;
L_0004: ldc.i4.0
L_0005: stloc.3
//goto L_0019
L_0006: br.s L_0019
//text=textArray[num2]
L_0008: ldloc.2
L_0009: ldloc.3
L_000a: ldelem.ref
L_000b: stloc.0
L_000c: nop
// Console.WriteLine(text)
L_000d: ldloc.0
L_000e: call void [mscorlib]System.Console::WriteLine(string)
L_0013: nop
L_0014: nop
// num2=num2+1
L_0015: ldloc.3
L_0016: ldc.i4.1
L_0017: add
L_0018: stloc.3
//flag=num2<textArray.Length
L_0019: ldloc.3
L_001a: ldloc.2
L_001b: ldlen
L_001c: conv.i4
L_001d: clt
// if (flag) goto L_0008
L_001f: stloc.s flag
L_0021: ldloc.s flag
L_0023: brtrue.s L_0008
//num=0
L_0025: ldc.i4.0
L_0026: stloc.1
// goto L_0037
L_0027: br.s L_0037
//textArray[num]=num.ToString()
L_0029: ldarg.1
L_002a: ldloc.1
L_002b: ldloca.s num
L_002d: callinstancestring [mscorlib]System.Int32::ToString()
L_0032: stelem.ref
//num=num+1
L_0033: ldloc.1
L_0034: ldc.i4.1
L_0035: add
L_0036: stloc.1
// flag=num<textArray.Length
L_0037: ldloc.1
L_0038: ldarg.1
L_0039: ldlen
L_003a: conv.i4
L_003b: clt
L_003d: stloc.s flag
//if (flag) goto L_0027
L_003f: ldloc.s flag
L_0041: brtrue.s L_0029
//返回
L_0043: ret
}
可以看出,params参数,就是一个数组,而对于foreach和for,其实现都是一样的,都是通过goto跳转来实现,事实上,所有的循环都是这种机制。
我们再来看看客户端的调用情况:
先说TestCase.TestParam("001", "002", "003");
//定义
.localsinit (
[0]classRefOutParamparam
……
[3] stringtext,
[4]classRefOutParamparam2,
[5] string[]textArray2,
……)
// textArray2=new string[3]
L_006c: ldloc.0
L_006d: ldc.i4.3
L_006e: newarr string
L_0073: stloc.s textArray2
// textArray2[0]=”001”
L_0075: ldloc.s textArray2
L_0077: ldc.i4.0
L_0078: ldstr"001"
L_007d: stelem.ref
// textArray2[1]=”002”
L_007e: ldloc.s textArray2
L_0080: ldc.i4.1
L_0081: ldstr"002"
L_0086: stelem.ref
// textArray2[2]=”003”
L_0087: ldloc.s textArray2
L_0089: ldc.i4.2
L_008a: ldstr"003"
L_008f: stelem.ref
// param.TestParam(textArray2)
L_0090: ldloc.s textArray2
L_0092: callvirtinstancevoid RefOutParam::TestParam(string[])
可以看出,在调用具有params参数的函数的时候,客户端先把这些params参数转换为一个数组,然后把该数组传递给被调用的参数。所以,我们可以推想,如果我们传递一个数组给TestParam函数,那么,该数组的内容应该被改变。而后面的结果证明了我们的想法。
现在,我们再看看lock关键字在MSIL中是如何实现的:
.localsinit(
[0]classRefOutParamparam,
……
[4]classRefOutParamparam2
……
)
// param = new RefOutParam();
L_0000: nop
L_0001: newobjinstancevoid RefOutParam::.ctor()
L_0006: stloc.0
//parma2=param
L_0007: ldloc.0
L_0008: dup
// lock (param2)
// {
// ……
// }
L_0009: stloc.s param2
L_000b: call void [mscorlib]System.Threading.Monitor::Enter(object)
……
L_00fc: leave.s L_0107
……
L_0100: call void [mscorlib]System.Threading.Monitor::Exit(object)
L_0105: nop
L_0106: endfinally
L_0107: nop
可以看出,lock关键字,实际上就是调用System.Threading.Monitor的Enter,Exit函数,所以,在多线程环境中,想避免死锁时就可以考虑使用System.Threading.Monitor.TryEnter。
总结:我们可能从来都不需要用到MSIL,但了解MSIL可以让我们了解许多其他人所不知道的内幕。
TAG: 方法参数
我的栏目
标题搜索
日历
|
|||||||||
日 | 一 | 二 | 三 | 四 | 五 | 六 | |||
1 | 2 | 3 | 4 | 5 | 6 | ||||
7 | 8 | 9 | 10 | 11 | 12 | 13 | |||
14 | 15 | 16 | 17 | 18 | 19 | 20 | |||
21 | 22 | 23 | 24 | 25 | 26 | 27 | |||
28 | 29 | 30 |
数据统计
- 访问量: 46567
- 日志数: 47
- 建立时间: 2009-09-03
- 更新时间: 2010-06-10