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

Google
        我们可能从来都不需要用到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
2 

对这些代码,我们先说说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

总结:我们可能从来都不需要用到MSIL,但了解MSIL可以让我们了解许多其他人所不知道的内幕。

posted @ 2007-09-13 08:15 永红 阅读(2610) 评论(26)  编辑 收藏 网摘 所属分类: .NetMSIL

  回复  引用    
#1楼 2007-08-30 13:31 | tes [未注册用户]
楼主不如去洗煤。
  回复  引用  查看    
#2楼 [楼主]2007-08-30 13:35 | 永红      
@tes
如果文章错误,请指出。这样讲话,对大家的技术提高没有好处。
  回复  引用  查看    
#3楼 2007-08-30 13:47 | henry      
觉得这样对学IL的帮助不大,应该在没有参考物的情况下用IL实现具体功能这样比较好深入了解使用.
  回复  引用    
#4楼 2007-08-30 13:52 | Fade [未注册用户]
一楼那句话什么意思,不大懂 -_-!
"你的成功是我工作的动力..."
楼主是当老师的?

  回复  引用  查看    
#5楼 [楼主]2007-08-30 13:53 | 永红      
@henry
不是为了学习IL,而是为了更好的理解C#.
  回复  引用  查看    
#6楼 [楼主]2007-08-30 13:54 | 永红      
@Fade
^_^。
N久以前确实是老师,还带过高3毕业班呢。
  回复  引用  查看    
#7楼 2007-08-30 13:57 | henry      
如果只是了解C#,那这些意义不大.
这已经是固定的东西,只需知道这几个关键如何用就可以.
因为我们根本无法改变这几个关键产生的IL.
  回复  引用  查看    
#8楼 [楼主]2007-08-30 14:06 | 永红      
@henry
我们确实是无法改变这几个关键字产生的IL,但是我们可以通过这几个关键字产生的IL知道更多的东西,比如对于lock,我们可以知道用System.Threading.Monitor.TryEnter在某些时候可能更加合适。另外,通过查看其IL代码,可以加深我们的理解,让我们对这些东西产生更加深刻的印象。
当然,我们并不能纯粹的为了C#而C#,在这个分析的过程中,我们还可以学到许多其他的东西。也许这并不是我们初衷,但我们确实学到了。
  回复  引用    
#9楼 2007-08-30 14:46 | cook [未注册用户]
完全看不懂il汇编,似乎唯一看到的一本il汇编的电子书还是e文的,郁闷。

不管怎么说了解一下il还是很有好处的。

楼主假如可以整理出来的话那是相当的好
  回复  引用    
#10楼 2007-08-30 14:52 | cook [未注册用户]
原来楼主已经整出来了,对不起对不起,偶要看看去
  回复  引用    
#11楼 2007-08-30 14:57 | .NET面试题 [未注册用户]
学习
  回复  引用    
#12楼 2007-08-30 16:12 | bighope [未注册用户]
曾经也试着追究过比较底层的东西,但是发现微软的东东根本不是在考虑底层效率上来做软件的,所以功能层更适用。过两天可能发现我们用来提高效率的方法完全被微软给封装了,欲用不能。
  回复  引用  查看    
#13楼 [楼主]2007-08-30 16:21 | 永红      
@cook
^_^
  回复  引用  查看    
#14楼 [楼主]2007-08-30 16:25 | 永红      
@bighope
我们如果想做一些对效率要求非常高的东西,那么,.net本来不是一个好的选择,1是内存消耗巨大,2是解释型的代码。不过,通过对底层的追究,可以让我们更好的使用我们现在所使用的工具。
  回复  引用  查看    
#15楼 [楼主]2007-08-30 16:26 | 永红      
@.NET面试题
大家共勉
  回复  引用    
#16楼 2007-08-30 17:11 | 傻子林 [未注册用户]
学习拉!~多谢分享
  回复  引用  查看    
#17楼 2007-08-30 19:47 | 曲滨*銘龘鶽      
一楼是网络爆民、拉出去河蟹
  回复  引用  查看    
#18楼 2007-08-30 20:59 | 搞IT的狐狸      
呵呵 有意思
  回复  引用    
#19楼 2007-08-30 22:19 | Linxi [未注册用户]
我只知道怎样用,原理不太清楚。这样可以吗?就像电是什么,99%的人说不清楚。管它是什么,安全使用就行。
  回复  引用  查看    
#20楼 [楼主]2007-08-31 08:26 | 永红      
@Linxi
清楚原理,能够让我们更好的、更安全的使用。
  回复  引用  查看    
#21楼 2007-08-31 10:05 | cwbboy      
楼主贴那么长IL代码,都不愿看,最后又不总结一下经过你对IL代码的分析上后得出的ref,out,params等关键字的更详细的区别。 所以这篇文章对于别人基本没有意义。 当然,楼主研究的精神值得鼓励。 
  回复  引用  查看    
#22楼 [楼主]2007-08-31 10:35 | 永红      
@cwbboy
不会吧。每段分析之后都有总结的哦。比如
“可以看出,lock关键字,实际上就是调用System.Threading.Monitor的Enter,Exit函数。”
“可以看出,params参数,就是一个数组,而对于foreach和for,其实现都是一样的,都是通过goto跳转来实现,事实上,所有的循环都是这种机制。”
……
……
可能是这种写作风格不好。
  回复  引用  查看    
#23楼 2007-08-31 11:19 | zzticzh      
学习就是进步!呵呵 支持
  回复  引用  查看    
#24楼 2007-12-11 17:26 | 欧尔      
还不错!
  回复  引用    
#25楼 2008-01-31 12:01 | Saiman [未注册用户]
不错, 可以加深对il的理解...

楼主, 要继续努力喔...

^o^
  回复  引用  查看    
#26楼 2008-06-06 00:14 | 簡簡單單..      
楼主您好! 不知道你有没有研究过 try-catch-finally 具体是怎么实现呢?

标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
Google站内搜索

China-pub 计算机图书网上专卖店!6.5万品种 2-8折!
近千种 9-95 新二手计算图书火热销售中!
开发者征途系统新作:《设计模式——基于C#的工程化实现及扩展》



相关文章:

相关链接: