從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關鍵字的方法。不敢班門弄斧,只是做一個完整的系列,所以按照自己理解的再整理一次。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM