2013年10月19日 星期六

IEnumerable 與 IEnumerator 介面如何實作foreach

前章 認識IL程式碼
在觀察Foreach中的IL我們發現了 Foreach中使用了
IEnumerable 與 IEnumerator 介面 本章將介紹
foreach中如何調用這兩種介面

OK! 首先 我們先來 談談 IEnumerable
可以看到此介面 只提供一個方法GetEnumerator
此方法會 傳回會逐一查看集合的列舉程式。

這邊要注意的是IEnumerator .GetEnumerator() 此方法
是回傳IEnumerator 而不是 IEnumerable
所以  IEnumerable只提供了 列舉的功能
其他的事情 將交給IEnumerator

再來 來看IEnumerator介面

假如 我們在之前的介面 已使用GetEnumerator() 取的列舉後
將列舉值傳到此介面 繼續後續動作
先來看看 IEnumerator 提供了 哪些方法??

bool MoveNext()
如果列舉值成功地前移至下一個項目,則為 true
如果列舉值已超過集合的結尾,則為 false
意思是說 他會有一個   參數++ >索引.Length
不斷累加 一子到超出索引範圍 才會=false

Reset 方法
設定列舉值至它的初始位置,這是在集合中第一個元素之前。
用於初始化 參數 ,這裡參數 同MoveNext()中累加的參數是同一個

Current 屬性
取得集合中目前的項目。
當MoveNext true時,將集合[參數值] 放入結果中

介紹完Enumeraor Enumerable
簡單來說 我們必須 使用 使用GetEnumerator()
取得矩陣後 再利用Enumerator介面所提供的方法

下面將實作GetEnumerator方法 讓我們能了解foreach中的運作原理
好的 首先 來看一下例題

首先 我們建立一個CS檔案內部實作類別如下

class MyCollection
    {
        private int[] item; //宣告 私用 整數 陣列叫 item,尚未初始化參數
        public MyCollection()
        {
            this.item = new int[5] {12, 44, 33, 2, 50};//初始化 剛剛宣告的 陣列
           
        }
        public MyEnumerator GetEnumerator()//foreach將會呼叫GetEnumerator()
        {                                  //並回傳MyEnumerator類別
            return new MyEnumerator(this);//將會實體化MyEnumerator並帶入(item)
        }
        public class MyEnumerator
        {
            int nIndex;
            MyCollection collection;
            public MyEnumerator(MyCollection coll)
            {
                ////當GetEnumerator()被呼叫 同時完成此區塊
                collection = coll;
                nIndex = -1;
            }
            public bool MoveNext()
            {
                nIndex++;
                return (nIndex < collection.item.GetLength(0));
            }
            public int Current
            {
                get
                { return (collection.item[nIndex]); }
            }
        }
    }


接下來 來看 主程式 呼叫副程式 副程式實作foreach
foreach將會 呼叫GetEnumerator()

程式碼如下
class Program
    {
        static void Sample_1()
        {
            Console.WriteLine("*-*-*-*-*-*-範例1 開始。"); Console.ReadLine();
            MyCollection col = new MyCollection();//實體化類別 並同事建立item陣列
            Console.WriteLine("集合中的值為:");
            // 顯示集合中的項目
            foreach (int i in col)// 逐步執行當執行到col時 foreach會呼叫GetEnumerator()
            {
                Console.WriteLine(i);
            }
            Console.WriteLine("*-*-*-*-*-*-範例1 結束。"); Console.ReadLine();
        }
         static void Main(string[] args)//主程式
        {
            Sample_1();//呼叫副程式
         }
    }




OK! 看完程式碼 想必 第一次 應該很亂吧!
讓我大概解析一下 他的步驟

首先 Program.cs 程式進入開端 main
裡面有一個sample_1();
他將會呼叫同 Program.cs程式檔中的類別
裡面的靜態成員static void Sample_1()
而這裡面 將會實體化 MyCollection類別
並且初始化類別內部item陣列

當實體化MyCollection col=new MyCollection
類別完後 將執行foreach中col的區塊
此時foreach,將會調用GetEnumerator()公開方法
此時 GetEnumerator()返回的是一個類中類MyEnumerator
GetEnumerator() 裡面將實體化new MyEnumerator(item)

程序便跳到 類中類MyEnumerator 裡面支援一個方法
MyEnumerator(MyCollection coll)
這方法中 將nindex 與collection 做初始化動作
待會這些初始化的參數 將被public bool MoveNext()調用
 
結束GetEnumerator()內部程序後 也初始化了MyEnumerator中的參數
並將 列舉數值,同樣返回MyEnumerator

這時候 foreach中col 呼叫GetEnumerator的工作已結束

接下來 將執行foreach中的in
此時 foreach 將呼叫bool MoveNext
此方法 返回是一個布林
取決於 剛剛初始化的nIndex是否小於列舉.length
此時nIndex++ 會不斷累加 一子到 陣列終點
MoveNext執行完後 回到foreach 中的in

如果之前MoveNext=false
挑出foreach迴圈 如果是 true
會接下去 執行foreach中的 int i 區塊

foreach將調用public int Current
他將回傳給foreach中的i
來看看Current內部程序 如何回傳
Get
   { return (collection.item[nIndex]); }
很清楚明白的 調用類別Collection中的item[]

之後i再回傳給 console.writeline(i)
結束!!

接下來我們來大概 看一下內部IL程式碼長啥樣?

.method private hidebysig static void  Sample_1() cil managed
{
  // Code size       118 (0x76)
  .maxstack  2
  .locals init ([0] class _002_foreach_test.nTest.MyCollection col,
           [1] int32 i,
           [2] class _002_foreach_test.nTest.MyCollection/MyEnumerator CS$5$0000,
           [3] bool CS$4$0001,
           [4] class [mscorlib]System.IDisposable CS$0$0002)
//上面宣告了4個V0=MyCollection col 類別
//           V1=foreach中的i
//           V2=類中類MyEnumerator
//           V3=bool 這是MoveNExt
//           V4=IDisposable清除資源用

  IL_0000:  nop
  IL_0001:  ldstr      bytearray (2A 00 2D 00 2A 00 2D 00 2A 00 2D 00 2A 00 2D 00   // *.-.*.-.*.-.*.-.
                                  2A 00 2D 00 2A 00 2D 00 C4 7B 8B 4F 31 00 20 00   // *.-.*.-..{.O1. .
                                  8B 95 CB 59 02 30 )                               // ...Y.0

//0001這裡是一個中文與符號的unicode代碼 將被下面console show出
  IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_000b:  nop
  IL_000c:  call       string [mscorlib]System.Console::ReadLine()
  IL_0011:  pop

  IL_0012:  newobj     instance void _002_foreach_test.nTest.MyCollection::.ctor()//實體化類別
  IL_0017:  stloc.0 //並存取col

  IL_0018:  ldstr      bytearray (C6 96 08 54 2D 4E 84 76 3C 50 BA 70 1A FF )       // ...T-N.v<P.p..
  IL_001d:  call       void [mscorlib]System.Console::WriteLine(string)
//0018是一個unicode 將被001d SHOW出

  IL_0022:  nop
  IL_0023:  nop
  IL_0024:  ldloc.0//讀取V0推入堆疊
  IL_0025:  callvirt   instance class _002_foreach_test.nTest.MyCollection/MyEnumerator _002_foreach_test.nTest.MyCollection::GetEnumerator()
//0025呼叫GetEnumerator()去實作內部區塊
//而這區塊是指向類中類的MyEnumerator
  IL_002a:  stloc.2//這邊V2也存一份列舉值
  .try
  {
    IL_002b:  br.s       IL_003d
    IL_002d:  ldloc.2 //讀取列舉
    IL_002e:  callvirt   instance int32 _002_foreach_test.nTest.MyCollection/MyEnumerator::get_Current()
    IL_0033:  stloc.1//將列舉[]放入V1
    IL_0034:  nop
    IL_0035:  ldloc.1//讀取V1將被下面0036SHOW出
    IL_0036:  call       void [mscorlib]System.Console::WriteLine(int32)
              //此後以此類推如法炮製
    IL_003b:  nop
    IL_003c:  nop

    IL_003d:  ldloc.2// 讀取剛剛存取的列舉值
    IL_003e:  callvirt   instance bool _002_foreach_test.nTest.MyCollection/MyEnumerator::MoveNext()
    IL_0043:  stloc.3//將MoveNext結果放入V3
    IL_0044:  ldloc.3//讀取V3
    IL_0045:  brtrue.s   IL_002d//true就跳到002d
    IL_0047:  leave.s    IL_0063//不然就跳出foreach迴圈
  }  // end .try
  finally
  {
    IL_0049:  ldloc.2
    IL_004a:  isinst     [mscorlib]System.IDisposable
    IL_004f:  stloc.s    CS$0$0002
    IL_0051:  ldloc.s    CS$0$0002
    IL_0053:  ldnull
    IL_0054:  ceq
    IL_0056:  stloc.3
    IL_0057:  ldloc.3
    IL_0058:  brtrue.s   IL_0062
    IL_005a:  ldloc.s    CS$0$0002
    IL_005c:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_0061:  nop
    IL_0062:  endfinally
  }  // end handler
  IL_0063:  nop
  IL_0064:  ldstr      bytearray (2A 00 2D 00 2A 00 2D 00 2A 00 2D 00 2A 00 2D 00   // *.-.*.-.*.-.*.-.
                                  2A 00 2D 00 2A 00 2D 00 C4 7B 8B 4F 31 00 20 00   // *.-.*.-..{.O1. .
                                  50 7D 5F 67 02 30 )                               // P}_g.0
  IL_0069:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_006e:  nop
  IL_006f:  call       string [mscorlib]System.Console::ReadLine()
  IL_0074:  pop
  IL_0075:  ret
} // end of method Program::Sample_1

以上是一個 簡單的例子 我們在MYcollection.cs
中沒使用到IEnumerator IEnumerable的
命名空間using System.Collections;
IL程式碼中可以證明!! 並無調用到Collections

接下來我們來看更進階的實做~~
這次實作interface 中的IEnumerator IEunmerable


一樣Main 中 Sample_2_a();呼叫副程式如下
static void Sample_2_a()
        {
            Console.WriteLine("*-*-*-*-*-*-範例2_a 開始。"); Console.ReadLine();
            MyCollection2_a col = new MyCollection2_a();
            Console.WriteLine("集合中的值為:");
            // 顯示集合中的項目
            foreach (int i in col)
            {
                Console.WriteLine(i);
            }
            Console.WriteLine("*-*-*-*-*-*-範例2_a 結束。"); Console.ReadLine();
        }

來看一下副程式所呼叫的類別CS程式檔中的類別如下
此類使用命名空間 collections

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
namespace _002_foreach_test.nTest
{
    // 宣告這個 collection 實做 IEnumerable 介面
    class MyCollection2_a : IEnumerable
    {
        private int[] items;
        public MyCollection2_a()
        {
            this.items = new int[5] { 12, 44, 33, 2, 50 };
        }
        public MyEnumerator2_a GetEnumrator()
        {
            return new MyEnumerator2_a(this);//實體化yEnumerator2_a(item)
                                             //並且傳回列舉
        }
        // 實做 GetEnumerator()
        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumrator();//呼叫自己的方法
            //等待回傳列舉給IEnumerator
        }
        // 宣告這個 enumerator 實做 IEnumerator 介面
        public class MyEnumerator2_a : IEnumerator
        {
            private int nIndex;
            MyCollection2_a collection;
            public MyEnumerator2_a(MyCollection2_a coll)
            {
                this.collection = coll;//當呼叫GetEnumerator()時
                this.nIndex = -1;      //此方法中的參數被初始化
            }
            #region IEnumerator 成員
            object IEnumerator.Current
            {
                get { return (this.collection.items[nIndex]); }
            }
            bool IEnumerator.MoveNext()
            {
                this.nIndex++;
                return (nIndex < collection.items.GetLength(0));
            }
            void IEnumerator.Reset()
            {
                this.nIndex = -1;
            }
            #endregion
        }
    }
}

 跟上一個範例差不多 不同於實作interface

來看一下IL 證明這次有使用到此命名空間與實作了interface
.method private hidebysig static void  Sample_2_a() cil managed
{
  // Code size       123 (0x7b)
  .maxstack  2
  .locals init ([0] class _002_foreach_test.nTest.MyCollection2_a col,
           [1] int32 i,
           [2] class [mscorlib]System.Collections.IEnumerator CS$5$0000,
           [3] bool CS$4$0001,
           [4] class [mscorlib]System.IDisposable CS$0$0002)
  IL_0000:  nop
  IL_0001:  ldstr      bytearray (2A 00 2D 00 2A 00 2D 00 2A 00 2D 00 2A 00 2D 00   // *.-.*.-.*.-.*.-.
                                  2A 00 2D 00 2A 00 2D 00 C4 7B 8B 4F 32 00 5F 00   // *.-.*.-..{.O2._.
                                  61 00 20 00 8B 95 CB 59 02 30 )                   // a. ....Y.0
  IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_000b:  nop
  IL_000c:  call       string [mscorlib]System.Console::ReadLine()
  IL_0011:  pop
  IL_0012:  newobj     instance void _002_foreach_test.nTest.MyCollection2_a::.ctor()
  IL_0017:  stloc.0
  IL_0018:  ldstr      bytearray (C6 96 08 54 2D 4E 84 76 3C 50 BA 70 1A FF )       // ...T-N.v<P.p..
  IL_001d:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0022:  nop
  IL_0023:  nop
  IL_0024:  ldloc.0
  IL_0025:  callvirt   instance class [mscorlib]System.Collections.IEnumerator [mscorlib]System.Collections.IEnumerable::GetEnumerator()
  IL_002a:  stloc.2
  .try
  {
    IL_002b:  br.s       IL_0042
    IL_002d:  ldloc.2
    IL_002e:  callvirt   instance object [mscorlib]System.Collections.IEnumerator::get_Current()
    IL_0033:  unbox.any  [mscorlib]System.Int32
    IL_0038:  stloc.1
    IL_0039:  nop
    IL_003a:  ldloc.1
    IL_003b:  call       void [mscorlib]System.Console::WriteLine(int32)
    IL_0040:  nop
    IL_0041:  nop
    IL_0042:  ldloc.2
    IL_0043:  callvirt   instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
    IL_0048:  stloc.3
    IL_0049:  ldloc.3
    IL_004a:  brtrue.s   IL_002d
    IL_004c:  leave.s    IL_0068
  }  // end .try
  finally
  {
    IL_004e:  ldloc.2
    IL_004f:  isinst     [mscorlib]System.IDisposable
    IL_0054:  stloc.s    CS$0$0002
    IL_0056:  ldloc.s    CS$0$0002
    IL_0058:  ldnull
    IL_0059:  ceq
    IL_005b:  stloc.3
    IL_005c:  ldloc.3
    IL_005d:  brtrue.s   IL_0067
    IL_005f:  ldloc.s    CS$0$0002
    IL_0061:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_0066:  nop
    IL_0067:  endfinally
  }  // end handler
  IL_0068:  nop
  IL_0069:  ldstr      bytearray (2A 00 2D 00 2A 00 2D 00 2A 00 2D 00 2A 00 2D 00   // *.-.*.-.*.-.*.-.
                                  2A 00 2D 00 2A 00 2D 00 C4 7B 8B 4F 32 00 5F 00   // *.-.*.-..{.O2._.
                                  61 00 20 00 50 7D 5F 67 02 30 )                   // a. .P}_g.0
  IL_006e:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0073:  nop
  IL_0074:  call       string [mscorlib]System.Console::ReadLine()
  IL_0079:  pop
  IL_007a:  ret
} // end of method Program::Sample_2_a
這裡IL內可以看到命名空間System.Collections
裡面有IEnumerator IEnumerable的介面
並且把它時做出來,這樣便能清楚了解它的運作原理
以上不多做介紹 ,因為大同小異

再來同上題,在加一點點小小的變化
IDisposable 並實作一個 Dispose
用意來釋放資源
Dispose將被GC下一次回收時回收
來看例題
一樣Sample_2_b();呼叫副程式

static void Sample_2_b()
        {          
            Console.WriteLine("*-*-*-*-*-*-範例2_b 開始。"); Console.ReadLine();
            MyCollection2_b col = new MyCollection2_b();
            Console.WriteLine("集合中的值為:");
            // 顯示集合中的項目
            foreach (int i in col)
            {
                Console.WriteLine(i);
            }
            Console.WriteLine("*-*-*-*-*-*-範例2_b 結束。"); Console.ReadLine();
        }


再來看MyCollection2_b.cs程式檔內部的類別與繼承的介面
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Collections;
namespace _002_foreach_test.nTest
{
    // 宣告這個 collection 實做 IEnumerable 介面
    class MyCollection2_b : IEnumerable
    {
        private int[] items;
        public MyCollection2_b()
        {
            this.items = new int[5] { 12, 44, 33, 2, 50 };
        }

        public MyEnumerator2_b MY_GetEnumerator()
        {
            return new MyEnumerator2_b(this);//取得this列舉
            //並且實體化MyEnumerator2_b(col)內部參數
        }

        // 實做 GetEnumerator()
        IEnumerator IEnumerable.GetEnumerator()
        {   //當執行到了foreach中的col會呼叫此方法
            return this.MY_GetEnumerator();//將方法接過去我們自製的方法

            //最後將列舉回傳給IEnumeraor()介面
        }

        // 宣告這個 enumerator 實做 IEnumerator 和 IDisposable 兩個介面
        public class MyEnumerator2_b : IEnumerator, IDisposable
        {
            private int nIndex;

            MyCollection2_b collection;
            public MyEnumerator2_b(MyCollection2_b coll)
            {
                this.collection = coll;//拷貝一份列舉給MyEnumerator2_b
                this.nIndex = -1;//將nIndex初始化
            }

            // 自有變數
            public int Current
            {
                get
                { return (collection.items[nIndex]); }
            }

            // 實做 IEnumerator
            #region IEnumerator 成員

            public void Reset()
            {
                this.nIndex = -1;
            }

            public bool MoveNext()
            {
                this.nIndex++;
                return (this.nIndex < collection.items.GetLength(0));
            }

            object IEnumerator.Current
            {
                get { return (Current); }
            }

            #endregion
            //public void Dispose()
            //{
            //    Console.WriteLine("處理中…");
            //    this.collection = null;
            //}

            // 實做 IDispos
            void IDisposable.Dispose()
            {
                //標註已不用的資源,待GC下一次回收
                Console.WriteLine("處理中…");
                this.collection = null;
            }

        }
       
    }
}


其實這一個範例已經篇題了=V="
IDispos GC 這又是另一門學問了

在來來看最後一個範例
這範例不再實作IEnumerator IEnumerable
直接正常使用foreach 順便看一下IL內部如何規劃

Sample_3() 呼叫副程式 ,內部程式如下

static void Sample_3()
        {
            Console.WriteLine("*-*-*-*-*-*-範例3 開始。"); Console.ReadLine();
            // 宣告一個 Hashtable
            Hashtable ziphash = new Hashtable();
            // 利用 Add() 加入項目 key,values
            ziphash.Add("234", "永和");
            ziphash.Add("613", "朴子");
            ziphash.Add("210", "北竿");
            ziphash.Add("100", "中正區");
            ziphash.Add("805", "旗津區");
            // 顯示內容
            Console.WriteLine("集合中的值為:");
            foreach (string zip in ziphash.Keys)//印出
            {
                Console.WriteLine(zip + "\t" + ziphash[zip]);
            }

            Console.WriteLine("*-*-*-*-*-*-範例3 結束。"); Console.ReadLine();
        }
S

接下來 可以觀察內部IL
將調用GetEnumeraor()方法取得Hashtable中的集合
IL程式碼因為大同小異 所以就不貼了
完!!

沒有留言:

張貼留言