从IL认识关键字(一)

背景

      网上流传“没有用.NET Reflector反编译并阅读过代码的程序员不是专业的.NET程序员” 。虽然是夸张手法,但是.NET Reflector确实是.Net程序员必不可少的一个工具。但是最近7.0后版本开始收费,功能是强大了,可以直接在VS上反编译看源代码。还有更多的功能没深入研究,但是再多的功能也抵不过编译成.net代码和IL。

关键字

     第一篇先来研究foreach这个关键字,可能大家都很熟悉,这个关键字的原理。但是既然要整理研究,就系统的整理一遍。若是已经熟悉这个关键字,可以返回。

集合遍历

C# 代码, 准备一个Student类,里面只有ID,Name属性

IList<Student> students = new List<Student>();

foreach (Student stu in students)
{
   Console.WriteLine(stu.Name);
}
  

由于IL代码较多,只贴上部分重要代码

L_0009: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> [mscorlib]System.Collections.Generic.IEnumerable`1<class Foreach.Student>::GetEnumerator()
L_000e: stloc.2
L_000f: br.s L_0026
L_0011: ldloc.2
L_0012: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1<class Foreach.Student>::get_Current()
L_0017: stloc.1
L_0018: nop
L_0019: ldloc.1
L_001a: callvirt instance string Foreach.Student::get_Name()
L_001f: call void [mscorlib]System.Console::WriteLine(string)
L_0024: nop
L_0025: nop
L_0026: ldloc.2
L_0027: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
L_002c: stloc.3
L_002d: ldloc.3
L_002e: brtrue.s L_0011
L_0030: leave.s L_0042
L_0032: ldloc.2
L_0033: ldnull
L_0034: ceq
L_0036: stloc.3
L_0037: ldloc.3
L_0038: brtrue.s L_0041
L_003a: ldloc.2
L_003b: callvirt instance void [mscorlib]System.IDisposable::Dispose()

.try L_000f to L_0032 finallyhandler L_0032 to L_0042

 从IL代码( L_0009   -- L_0030) 看到foreach其实就是一个Enumerator枚举器的遍历。

翻译后C#代码

IList<Student> students = new List<Student>();

IEnumerator<Student> enumerator = students.GetEnumerator();
try
{
    while (enumerator.MoveNext())
    {
        Student stu = enumerator.Current as Student;
        Console.WriteLine(stu.Name);
    }
}
finally
{
    enumerator.Dispose();
}

 翻译后代码已与重新反编译成IL与foreach的IL几乎一致

(注:foreach生成的IL,不仅foreach,还有其他关键字。会比直接写代码多了一些nop指令,网上查阅查阅这个指令的意义是 Do nothing.既然是Do nothing即不影响程序运行,我认为可以忽略)

验证代码

   虽然说可以直接将IL代码翻译过来,但是我们还要保持一颗怀疑的态度,包括怀疑自己。下面是验证代码:

   1)自己实现IEnumerator,IEnumerable类,输出Current属性与MoveNext()

   下面准备一个Students类实现IEnumerator,IEnumerable接口

   关于IEnumerator和IEnumerable的区别,网上也有很多解释,我的理解大约就是

   IEnumerable:实现IEnumerable才能实现枚举 

   IEnumerator:实现一个枚举器

   

View Code
class Students : IEnumerator<Student>, IEnumerable<Student>
    {
        private static List<Student> list = new List<Student>();
        static Students()
        {
            list.Add(new Student() { ID = 1, Name = "Jack" });
            list.Add(new Student() { ID = 2, Name = "Rose" });
        }

        private int index = 0;
        private Student current;

        #region IEnumerator接口

        public Student Current
        {
            get
            {
                Console.WriteLine("----------Current----------");
                return this.current;
            }
        }

        object System.Collections.IEnumerator.Current
        {
            get { return this.current; }
        }

        public bool MoveNext()
        {
            Console.WriteLine("----------MoveNext----------");
            if (this.index < list.Count)
            {
                this.current = list[this.index];
                this.index++;
                return true;
            }
            return false;
        }

        public void Reset()
        {
            this.index = 0;
            this.current = null;
        }

        public void Dispose()
        {

        }

        #endregion

        #region IEnumerable接口

        public IEnumerator<Student> GetEnumerator()
        {
            return this;
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this;
        }

        #endregion
    }

 运行代码截图如下,和我们之前预想一致

 

数组遍历

   经过园友提醒,foreach漏了还有数组的遍历,数组遍历也是我们平常最常用的。在这里加上数组的遍历。下面是简单一个例子

public void EeachArray()
{
    foreach(int item in nums)
    {
    }
}

  对应的IL如下:

.locals init (
    [0] int32 num,
    [1] int32[] numArray,
    [2] int32 num2,
    [3] bool flag)
L_0000: nop 
L_0001: nop 
L_0002: ldarg.0 
L_0003: ldfld int32[] Test.Program::nums
L_0008: stloc.1 
L_0009: ldc.i4.0 
L_000a: stloc.2 
L_000b: br.s L_0017
L_000d: ldloc.1 
L_000e: ldloc.2 
L_000f: ldelem.i4 
L_0010: stloc.0 
L_0011: nop 
L_0012: nop 
L_0013: ldloc.2 
L_0014: ldc.i4.1 
L_0015: add 
L_0016: stloc.2 
L_0017: ldloc.2 
L_0018: ldloc.1 
L_0019: ldlen 
L_001a: conv.i4 
L_001b: clt 
L_001d: stloc.3 
L_001e: ldloc.3 
L_001f: brtrue.s L_000d
L_0021: ret 

   这里分为三个部分

  1. (L_0000  --  L_000b) : 初始化变量,跳转到步骤3
  2. (L_000d  --  L_0016) : 取出数组索引为局部变量索引2(即num2)的值并赋值局部变量索引为1(即num),局部变量索引为2(即num2) 加一
  3. (L_0017  --  L_001f)  : 局部变量索引为2(即num2) 与 数组长度比较,若小于跳转步骤2,否则结束

  从上面分析可知,这是一个循环结构,并在循环体取出数组值,上面IL大概是下面形式:

for(int num1 = 0; num1 < array.Length; num1++)
{
    int num= array[num1];
}

延伸

   我们知道除了上面那种方法,遍历枚举外,还有一种常用的方法遍历枚举

public IEnumerator<Student> GetEnumerator()
{
    for (int i = 0; i < list.Count; i++)
    {
        yield return list[i];
    }
}

 其实yield字段会自动生成一个实现IEnumerator类,所以这种方法最终的枚举器还是IEnumerator,这只是.Net的语法糖

下一篇关键字

      既然这里提到yield关键字,下一篇写yield的关键字。关于yield园子里的老赵已经详细解释过人肉反编译使用yield关键字的方法。不敢班门弄斧,只是做一个完整的系列,所以按照自己理解的再整理一次。

原文地址:https://www.cnblogs.com/WilsonPan/p/2889311.html