程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> C#迭代器

C#迭代器

編輯:C#入門知識

C#迭代器


在.NET中,迭代器模式是通過IEnumerator和IEnumerable接口以及它們的泛型版本來實現的。如果某個類實現了IEnumerable接口,就說明它可以被迭代訪問,調用GetEnumerator()方法將返回IEnumerator的實現,這個就是迭代器本身。   在C# 1.0中,利用foreach語句實現了訪問迭代器的內置支持,讓集合的遍歷變得簡單、明了。其實,foreach的實現就是調用GetEnumerator和MoveNext方法以及Current屬性。所以說,在C# 1.0中要獲得迭代器就必須實現IEnumerable接口中的GetEnumerator方法,要實現一個迭代器就要實現IEnumerator接口中的MoveNext和Reset方法   在C# 2.0中提供的語法糖來簡化迭代器的實現,可以通過yield關鍵字來簡化迭代器的實現。   C# 1.0中的迭代器實現 假設我們要實現一個字符列表類型,並且可以通過foreach來遍歷這個類型。那麼,在C# 1.0中,就要實現IEnumerable和IEnumerator接口。   復制代碼 namespace IteratorTest {     class Program     {         static void Main(string[] args)         {             CharList charList = new CharList("Hello World");             foreach (var c in charList)             {                 Console.WriteLine(c);             }               Console.Read();         }     }       class CharList : IEnumerable     {         public string TargetStr { get; set; }           public CharList(string str)         {             this.TargetStr = str;         }           public IEnumerator GetEnumerator()         {             return new CharIterator(this.TargetStr);         }     }       class CharIterator : IEnumerator     {         //引用要遍歷的字符串         public string TargetStr { get; set; }         //指出當前遍歷的位置         public int position { get; set; }           public CharIterator(string targetStr)         {             this.TargetStr = targetStr;             this.position = this.TargetStr.Length;         }           public object Current         {             get             {                 if (this.position == -1 || this.position == this.TargetStr.Length)                 {                     throw new InvalidOperationException();                 }                 return this.TargetStr[this.position];             }         }           public bool MoveNext()         {             //如果滿足繼續遍歷的條件,設置position的值             if (this.position != -1)             {                 this.position--;             }             return this.position > -1;         }           public void Reset()         {             this.position = this.TargetStr.Length;         }     } }   在上面的例子中,CharIterator就是迭代器的實現,position字段存儲當前的迭代位置,通過Current屬性可以得到當前迭代位置的元素,MoveNext方法用於更新迭代位置,並且查看下一個迭代位置是不是有效的。   當我們通過VS單步調試下面語句的時候   foreach (var c in charList) 代碼首先執行到foreach語句的charList處獲得迭代器CharIterator的實例,然後代碼執行到in會調用迭代器的MoveNext方法,最後變量c會得到迭代器Current屬性的值;前面的步驟結束後,會開始一輪新的循環,調用MoveNext方法,獲取Current屬性的值。   C# 2.0通過yield簡化迭代器實現 通過C# 1.0中迭代器的代碼看到,要實現一個迭代器就要實現IEnumerator接口,然後實現IEnumerator接口中的MoveNext、Reset方法和Current屬性。   在C# 2.0中可以直接使用yield語句來簡化迭代器的實現。     class CharList : IEnumerable {     public string TargetStr { get; set; }       public CharList(string str)     {         this.TargetStr = str;     }       public IEnumerator GetEnumerator()     {         for (int index = this.TargetStr.Length; index > 0; index--)         {             yield return this.TargetStr[index-1];         }     } }   通過上面的代碼可以看到,通過使用yield return語句,我們可以替換掉整個CharIterator類。   yield return語句就是告訴編譯器,要實現一個迭代器塊。如果GetEnumerator方法的返回類型是非泛型接口,那麼迭代器塊的生成類型(yield type)是object,否則就是泛型接口的類型參數。   通過IL代碼可以看到,對於yield return語句語句,編譯器為我們生成了一個嵌套的類型(nested type) <GetEnumerator>d__0,並且這個類實現了IEnumerator接口。       當編譯器遇到迭代塊時,它創建了一個實現了狀態機的內部類。這個類記住了我們迭代器的准確當前位置以及本地變量,包括參數。這個類有點類似與C# 1.0中手寫的那段代碼,它將所有需要記錄的狀態保存為實例變量。為了實現一個迭代器,這個狀態機需要按順序執行的操作:   它必須具有某個初始狀態 當MoveNext被調用時,他需要執行GetEnumerator方法中的代碼來准備下一個待返回的數據 當調用Current屬性是,它必須返回上一個生成的數據 需要知道什麼時候迭代結束,MoveNext會返回false 注意,當我們想要避免迭代器中的裝箱和拆箱是,就要實現迭代器的泛型版本,由於泛型IEnumerable <T>接口繼承了泛型型IEnumerable接口,我們需要在泛型迭代器代碼中加入   System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {   return GetEnumerator(); } 這樣,非泛型方法轉而調用泛型方法,從而不需要再去實現非泛型的IEnumerable接口了。   迭代器的工作流程 前面簡單提到了迭代器的工作流程,下面我們通過一個例子進一步看看迭代器工作流程。     class Program {     static readonly String Padding = new String(' ', 30);     static IEnumerable<Int32> CreateEnumerable()     {         Console.WriteLine("{0}Start of CreateEnumerable", Padding);         for (int i = 0; i < 3; i++)         {             Console.WriteLine("{0}About to yield {1}", Padding, i);             yield return i;             Console.WriteLine("{0}After yield", Padding);         }         Console.WriteLine("{0}Yielding final value", Padding);         yield return -1;         Console.WriteLine("{0}End of CreateEnumerable()", Padding);     }       static void Main(string[] args)     {         IEnumerable<Int32> iterable = CreateEnumerable();         IEnumerator<Int32> iterator = iterable.GetEnumerator();         Console.WriteLine("Starting to iterate");         while (true)         {             Console.WriteLine("Calling MoveNext()...");             Boolean result = iterator.MoveNext();             Console.WriteLine("...MoveNext result={0}", result);             if (!result)             {                 break;             }             Console.WriteLine("Fetching Current...");             Console.WriteLine("...Current result={0}", iterator.Current);         }         Console.Read();     } }   一般迭代器都會結合foreach語句,然後foreach會在最後調用Dispose方法。這裡為了演示,代碼中使用while語句實現循環。   稍微打斷一下,插入一個內容的介紹,通常為了實現IEnumerable,我們只會返回IEnumerator;如果僅僅是在方法中生成一個序列,可以返回IEnumerable。所以將代碼改為下面的方式也可以工作:     static IEnumerator<Int32> CreateEnumerable() {     …… }   …… //IEnumerable<Int32> iterable = CreateEnumerable(); IEnumerator<Int32> iterator = CreateEnumerable();   兩種方式的IL代碼是不同的,這裡只列出了編譯器內嵌類型實現了那些接口,更詳細的內容可以通過ILSpy查看:   返回IEnumerable     .class nested private auto ansi sealed beforefieldinit '<CreateEnumerable>d__0'     extends [mscorlib]System.Object     implements class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>,                [mscorlib]System.Collections.IEnumerable,                class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>,                [mscorlib]System.Collections.IEnumerator,                [mscorlib]System.IDisposable {……}   返回IEnumerator   .class nested private auto ansi sealed beforefieldinit '<CreateEnumerable>d__0'     extends [mscorlib]System.Object     implements class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>,                [mscorlib]System.Collections.IEnumerator,                [mscorlib]System.IDisposable {……} 回到這個例子,程序的輸出結果為:       在這段代碼中有幾個注意點:   直到第一次調用MoveNext,CreateEnumerable中的方法才被調用 在調用MoveNext的時候,已經做好了所有操作,獲取Current屬性並沒有執行任何代碼 代碼在yield return之後就停止執行,在下一次調用MoveNext方法的時候繼續執行 同一個方法的不同地方可以有多個yield return語句 代碼不會在最後的yield return處結束,而是通過返回false的MoveNext調用來結束方法的執行 第一點尤為重要:這意味著如果在方法調用時需要立即執行,就不能使用迭代器塊。例如如果將參數驗證放在迭代塊中,那麼他將不能夠很好的起作用,這是經常會導致的錯誤的地方,而且這種錯誤不容易發現。   進一步了解迭代器工作流程 在常規方法中,return語句通常有兩種作用:一是返回調用者執行的結果。二是終止方法的執行,在終止之前執行finally語句中的方法。在上面的例子中,我們看到了yield return語句只是短暫的退出了方法,在調用MoveNext的時候繼續執行。根本沒有檢查finally代碼塊的行為。   如何真正的退出方法?退出方法時finnally語句塊如何執行?下面來看看一個比較簡單的結構:yield break語句塊。   使用yield return結束迭代器執行 通常方法只有一個退出點,不過有時候我們想"提早退出。對於迭代器塊,使用yield break就能達到想要的效果。它能夠馬上終止迭代,使得下一次調用MoveNext的時候返回false。   下面的代碼演示了從1迭代到100,但是時間超時的時候就停止了迭代:     class Program {     static IEnumerable<Int32> CountWithTimeLimit(DateTime limit)     {         for (int i = 1; i <= 100; i++)         {             if (DateTime.Now >= limit)             {                 yield break;             }             yield return i;         }              }       static void Main(string[] args)     {         DateTime stop = DateTime.Now.AddSeconds(2);         foreach (Int32 i in CountWithTimeLimit(stop))         {             Console.WriteLine("Received {0}", i);             Thread.Sleep(300);         }         Console.WriteLine("End of Main");         Console.Read();     } }   從程序的輸出可以看到,yield break語句的行為類似於普通方法的return,迭代器的迭代被停止,並提前退出。   下面就看看finally語句塊是如何執行以及何時執行的。   finally代碼塊的執行 通常,finally語句塊在當方法執行退出特定區域時就會執行。迭代塊中的finally語句和普通方法中的finally語句塊不一樣。就像我們看到的,yield return語句停止了方法的執行,而不是退出方法,根據這一邏輯,在這種情況下,finally語句塊中的語句不會執行。   但當碰到yield break語句的時候,就會執行finally 語句塊。一般在迭代塊中使用finally語句來釋放資源,就像使用using語句一樣。   下面修改前面的例子來看finally語句如何執行。不管是迭代到了100次或者是由於時間到了停止了迭代,或者是拋出了異常,finally語句總會執行。       static IEnumerable<Int32> CountWithTimeLimit(DateTime limit) {     try     {         for (int i = 1; i <= 100; i++)         {             if (DateTime.Now >= limit)             {                 yield break;             }             yield return i;         }     }     finally     {         Console.WriteLine("Stopping");     } }   只有在調用MoveNext後迭代塊中的語句才會執行,那麼如果不掉用MoveNext呢,如果調用幾次MoveNext然後停止調用,結果會怎麼樣呢?看下面一段代碼     DateTime stop = DateTime.Now.AddSeconds(2); foreach (Int32 i in CountWithTimeLimit(stop)) {     if (i > 3)     {         Console.WriteLine("Returning");         return;     }     Thread.Sleep(300); }   在上面代碼中,我們不是提前停止執行迭代器塊,而是提前停止使用迭代器。在foreach循環中的return語句執行後,迭代器的finally代碼也被執行了。   之所以這裡的finally被執行了,是因為foreach會在自己的finall代碼塊中調用IEnumerator 所提供的Dispose方法。當迭代器完成迭代之前,如果調用由迭代器代碼塊創建的迭代器上的Dispose方法,那麼狀態機就會執行在代碼當前"暫停"位置范圍內的任何finally代碼塊。這有點復雜,但是結果很容易解釋:只要使用foreach循環,迭代塊中的finally塊會如期望的那樣執行。   上面的描述可以通過下面的代碼進行驗證:     static void Main(string[] args) {     DateTime stop = DateTime.Now.AddSeconds(2);     IEnumerable<Int32> iterable = CountWithTimeLimit(stop);     IEnumerator<Int32> iterator = iterable.GetEnumerator();       iterator.MoveNext();     Console.WriteLine("Reveived {0}", iterator.Current);       iterator.MoveNext();     Console.WriteLine("Reveived {0}", iterator.Current);       //顯示調用Dispose來執行迭代器的finally代碼塊     //iterator.Dispose();       Console.WriteLine("End of Main");     Console.Read(); }   總結 本文中看到了C# 1.0中如何實現一個迭代器,以及通過C# 2.0中提供的yield return如何簡化一個迭代器的實現。通過對迭代器工作流程的介紹,看到了yield return的延遲執行,yield return語句只表示"暫時的"退出方法。    

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved