了解MSIL可以让我们了解许多其他人所不知道的内幕。本文就试图通过MSIL,剥开一些披在C#上面的漂亮外衣。 

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

对这些代码,我们先说说refout,这个已经被别人讲了许多次了,我再重复一下(领导讲话时经常这样^_^):

TestCase.NoRef(i);没有用ref/out,那么,在函数体中对参数的更改,其有效范围只在当前函数体内,出了该函数,参数的值便不再保留。

TestCase.TestRef(ref i); TestCase.TestOut(out i);用了ref/out参数后,在函数体中对参数的更改,出了该函数后仍然有效。用MSDN的说法:“ref 关键字使参数按引用传递。……out 关键字会导致参数通过引用来传递……传递到 ref 参数的参数必须最先初始化。这与 out 不同,out 的参数在传递之前不需要显式初始化……尽管作为 out 参数传递的变量不需要在传递之前进行初始化,但需要调用方法以便在方法返回之前赋值……ref out 关键字在运行时的处理方式不同,但在编译时的处理方式相同。”这一大段话,可以总结为“refout参数都是通过引用传值,ref参数在调用前必须初始化,out参数在返回前必须初始化,refout参数的编译处理相同,但是在运行时的处理方式不同”。通过reflector反汇编,NoRefTestRefTestOutMSIL代码如下:

.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
}

可以看出,refout参数都被编译成地址了。对这些参数的操作,都是在操作其地址,而不是该参数的值,所以,对这些参数的更改,实际上就是更改了相应参数的地址所指向的值。另外,在函数体的内部,对refout参数操作的指令是完全相同的。而没有用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参数的函数时,先取得参数的地址,然后把该地址传给被调用参数。

 

现在再看paramsforeachfor,我们先看TestParamMSIL代码:

.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参数,就是一个数组,而对于foreachfor,其实现都是一样的,都是通过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.MonitorEnterExit函数,所以,在多线程环境中,想避免死锁时就可以考虑使用System.Threading.Monitor.TryEnter

posted on 2007-08-31 09:56  BlueTzar  阅读(528)  评论(0)    收藏  举报