用MSIL剥开C#的外衣(一):方法参数ref、out、params和lock、for和foreach关键字

       我们可能从来都不需要用到MSIL,但了解MSIL可以让我们了解许多其他人所不知道的内幕。本文就试图通过MSIL,剥开一些披在C#上面的漂亮外衣。 
对于方法参数,MSDN上这样说:“ 如果在为方法声明参数时未使用 ref out ,则该参数可以具有关联的值。可以在方法中更改该值,但当控制传递回调用过程时,不会保留更改的值。通过使用方法参数关键字,可以更改这种行为 。” 这样说太抽象了,现在举一个例子来进行说明:
using System;
public class RefOutParam
{
    public void NoRef(int i)
    {
        i = 500;
    }
    public void TestRef(ref int i)
    {
        i = 100;
    }
    public void TestOut(out int i)
    {
        i = 200;
    }
    public void TestParam(params string[] Fields)
    {
        foreach (string field in Fields)
        {
            Console.WriteLine(field);
        }
        for (int i = 0; i < Fields.Length; i++)
            Fields[i] = i.ToString();
    }
    public static void Main()
   {
        RefOutParam TestCase = new RefOutParam();
        lock (TestCase)
        {
            int i = 0;
            TestCase.NoRef(i);
            Console.WriteLine("testing no ref ...i={0}", i);  
            TestCase.TestRef(ref i);
            Console.WriteLine("testing ref.... i={0}", i);  
            TestCase.TestOut(out i);
            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 (string s in TestParams)
                Console.WriteLine(s);
        }
    }
}
输出结果不说大家也知道,那就是:
testing no ref ...i=0
testing ref.... i=100
testing out.... i=200
testing param 001
001
002
003
testing param 002
001
002
003
0
1
对这些代码,我们先说说ref和out,这个已经被别人讲了许多次了,我再重复一下(领导讲话时经常这样^_^):
TestCase.NoRef(i);没有用ref/out,那么,在函数体中对参数的更改,其有效范围只在当前函数体内,出了该函数,参数的值便不再保留。
TestCase.TestRef(ref i); TestCase.TestOut(out i);用了ref/out参数后,在函数体中对参数的更改,出了该函数后仍然有效。用MSDN的说法:“ ref 关键字使参数按引用传递。…… out 关键字会导致参数通过引用来传递……传递到 ref 参数的参数必须最先初始化。这与 out 不同, out 的参数在传递之前不需要显式初始化……尽管作为 out 参数传递的变量不需要在传递之前进行初始化,但需要调用方法以便在方法返回之前赋值…… ref out 关键字在运行时的处理方式不同,但在编译时的处理方式相同。”这一大段话,可以总结为“ ref out 参数都是通过引用传值, ref 参数在调用前必须初始化, out 参数在返回前必须初始化, ref out 参数的编译处理相同,但是在运行时的处理方式不同”。通过 reflector 反汇编, NoRef TestRef TestOut MSIL 代码如下:
.method public hidebysig instance void NoRef(int32 i) cil managed
{
    .maxstack 8
 L_0000: nop 
//把值500装入堆栈
 L_0001: ldc.i4 500
//把所提供的值(500)存入参数槽i所在的位置
    L_0006: starg.s i
    L_0008: ret 
}  
.method public hidebysig instance void TestRef(int32& i) cil managed
{
    .maxstack 8
 L_0000: nop 
//把类型为int32的地址参数i装入堆栈
 L_0001: ldarg.1 
//把值100装入堆栈
 L_0002: ldc.i4.s 100
//把所提供的值(100)存入堆栈中的地址(i)
    L_0004: stind.i4 
    L_0005: ret 
}
.method public hidebysig instance void TestOut([out] int32& i) cil managed
{
    .maxstack 8
 L_0000: nop 
//把类型为int32的地址参数i装入堆栈
 L_0001: ldarg.1 
//把值200装入堆栈
 L_0002: ldc.i4 200
//把所提供的值(200)存入堆栈中的地址(i)
    L_0007: stind.i4 
    L_0008: ret 
}
可以看出,ref和out参数都被编译成地址了。对这些参数的操作,都是在操作其地址,而不是该参数的值,所以,对这些参数的更改,实际上就是更改了相应参数的地址所指向的值。另外,在函数体的内部,对ref和out参数操作的指令是完全相同的。而没有用ref的函数,对参数的操作其实就是对参数槽的操作,并不影响到参数本身。 
客户端调用的MSIL代码如下:
.locals init (
        [0] class RefOutParam param, 
        [1] int32 num, //int num;
……
//TestCase.TestRef(ref i);
    L_002d: ldloc.0 
    L_002e: ldloca.s num  //把num的地址装入堆栈
    L_0030: callvirt instance void RefOutParam::TestRef(int32&)
// TestCase.TestOut(out i);
    L_0047: ldloc.0 
    L_0048: ldloca.s num //把num的地址装入堆栈
    L_004a: callvirt instance void RefOutParam::TestOut(int32&)
 
可以看出,客户端在调用具有ref/out参数的函数时,先取得参数的地址,然后把该地址传给被调用参数。
 
现在再看params、foreach和for,我们先看TestParam的MSIL代码:
.method public hidebysig instance void TestParam(string[] Fields) cil managed
{
 //变量定义
    .param [1]
    .custom instance void [mscorlib]System.ParamArrayAttribute::.ctor()
    .maxstack 3
    .locals init (
        [0] string text,
        [1] int32 num,
        [2] string[] textArray,
        [3] int32 num2,
        [4] bool flag)
    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: call instance string [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");
//定义
.locals init (
  [0] class RefOutParam param
     ……
        [3] string text,
        [4] class RefOutParam param2,
        [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: callvirt instance void RefOutParam::TestParam(string[]) 
可以看出,在调用具有params参数的函数的时候,客户端先把这些params参数转换为一个数组,然后把该数组传递给被调用的参数。所以,我们可以推想,如果我们传递一个数组给TestParam函数,那么,该数组的内容应该被改变。而后面的结果证明了我们的想法。 
现在,我们再看看lock关键字在MSIL中是如何实现的:
    .locals init (
        [0] class RefOutParam param,
     ……
        [4] class RefOutParam param2
……
)
// param = new RefOutParam();
    L_0000: nop 
    L_0001: newobj instance void 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可以让我们了解许多其他人所不知道的内幕。
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值