枚舉器和迭代器
枚舉器和可枚舉類型
第12章中,我們看到可以用foreach語句遍歷數組。在本章,我們會進一步探討數組,來看看為什么它們可以被foreach語句處理。我們還會研究如何使用迭代器為用戶自定義類增加該功能。
foreach語句
數組foreach語句為我們依次取出數組中的每個元素。
int[] arr1={10,11,12,13}; foreach(int item in arr1) { Console.WriteLine("Item value: {0}",item); }
為什么數組可以這么做?因為數組可以按需提供一個叫做枚舉器(enumerator)的對象。枚舉器可以依次返回請求的數組中的元素。枚舉器“知道”項的次序並且跟蹤它在序列中的位置,然后返回請求的當前項。
對於由枚舉器的類型,必須有一個方法來獲取它。獲取對象枚舉器的方法是調用對象的GetEnumerator方法。實現GetEnumerator方法的類型叫做可枚舉類型(enumerable type或enumerable)。數組是可枚舉類型。
foreach結構設計用來和可枚舉類型一起使用。只要給它的遍歷對象是可枚舉類型,它就會執行如下行為:
- 調用GetEnumerator獲取對象枚舉器
- 從枚舉器中請求每一項並且把它作為迭代變量(iteration variable),代碼可以讀取該變量但不能改變
IEnumerator接口
實現了IEnumerator接口的枚舉器包含3個函數成員:Current、MoveNext、Reset。
- Current是返回序列中當前位置項的屬性
- 它是只讀屬性
- 它返回object類型的引用,所以可以返回任何類型
- MoveNext是把枚舉器為知前進到集合中下一項的方法。它返回布爾值,指示新的位置是有效位置還是已超過序列尾部
- 如果新位置有效,返回true
- 如果新位置無效,返回false
- 枚舉器的原始位置在序列第一項之前,依次MoveNext必須在第一次使用Current前調用
- Reset是把位置重置為原始狀態的方法
枚舉器與序列中的當前項保持聯系的方式完全取決於實現。可以通過對象引用、索引值或其他方式來實現。對於內置的一維數組來說,就是用項的索引。
有了集合的枚舉器,我們就可以使用MoveNext和Current成員來模仿foreach循環遍歷集合中的項。
例:手動做foreach語句自動做的事情。
class Program { static void Main() { int[] MyArray={10,11,12,13}; IEnumerator ie=MyArray.GetEnumerator(); while(ie.MoveNext()) { int i=(int)ie.Current; Console.WriteLine("{0}",i); } } }
IEnumerator接口
可枚舉類是指實現了IEnumerator接口的類。IEnumerator接口只有一個成員–GetEnumerator方法,它返回對象的枚舉器。
使用IEnumerable和IEnumerator的示例
下面是個可枚舉類的完整示例,類名Spectrum,枚舉器類為ColorEnumerator。
using System; using System.Collections; class ColorEnumerator:IEnumerator { string[] _colors; int _position=-1; public ColorEnumerator(string[] theColors) { _colors=new string[theColors.Length]; for(int i=0;i<theColors.Length;i++) { _colors[i]=theColors[i]; } } public object Current { get { if(_position==-1||_position>=_colors.Length) { throw new InvalidOperationException(); } return _colors[_position]; } } public bool MoveNext() { if(_position<_colors.Length-1) { _position++; return true; } else { return false; } } public void Reset() { _position=-1; } } class Spectrum:IEnumerable { string[] Colors={"violet","blue","cyan","green","yellow","orange","red"}; public IEnumerator GetEnumerator() { return new ColorEnumerator(Colors); } } class Program { static void Main() { var spectrum=new Spectrum(); foreach(string color in spectrum) { Console.WriteLine(color); } } }
泛型枚舉接口
目前我們描述的枚舉接口都是非泛型版本。實際上,在大多數情況下你應該使用泛型版本IEnumerable<T>
和IEnumerator<T>
。它們叫做泛型是因為使用了C#泛型(參見第17章),其使用方法和非泛型形式差不多。
兩者間的本質差別如下:
- 對於非泛型接口形式:
- IEnumerable接口的GetEnumerator方法返回實現IEnumerator枚舉器類的實例
- 實現IEnumerator的類實現了Current屬性,它返回object的引用,然后我們必須把它轉化為實際類型的對象
- 對於泛型接口形式:
IEnumerable<T>
接口的GetEnumerator方法返回實現IEnumator<T>
的枚舉器類的實例- 實現
IEnumerator<T>
的類實現了Current屬性,它返回實際類型的對象,而不是object基類的引用
需要重點注意的是,我們目前所看到的非泛型接口的實現不是類型安全的。它們返回object類型的引用,然后必須轉化為實際類型。
而泛型接口的枚舉器是類型安全的,它返回實際類型的引用。如果要創建自己的可枚舉類,應該實現這些泛型接口。非泛型版本可用於C#2.0以前沒有泛型的遺留代碼。
盡管泛型版本和非泛型版本一樣簡單易用,但其結構略顯復雜。
迭代器
可枚舉類和枚舉器在.NET集合類中被廣泛使用,所以熟悉它們如何工作很重要。不過,雖然我們已經知道如何創建自己的可枚舉類和枚舉器了,但我們還是很高興聽到,C#從2.0版本開始提供了更簡單的創建枚舉器和可枚舉類型的方式。
實際上,編譯器將為我們創建它們。這種結構叫做迭代器(iterator)。我們可以把手動編碼的可枚舉類型和枚舉器替換為由迭代器生成的可枚舉類型和枚舉器。
在解釋細節前,我們先看兩個示例。下面的方法實現了一餓產生和返回枚舉器的迭代器。
- 迭代器返回一個泛型枚舉器,該枚舉器返回3個string類型的項
- yield return語句聲明這是枚舉中的下一項
public IEnumerator<string>BlackAndWhite() { yield return "black"; yield return "gray"; yield return "white"; }
下面方法聲明了另一個版本,並輸出相同結果:
public IEnumerator<string>BlackAndWhite() { string[] theColors={"black","gray","white"}; for(int i=0;i<theColors.Length;i++) { yield return theColors[i]; } }
迭代器塊
迭代器塊是有一個或多個yield語句的代碼塊。下面3種類型的代碼塊中的任意一種都可以是迭代器塊:
- 方法主體
- 訪問器主體
- 運算符主體
迭代器塊與其他代碼塊不同。其他塊包含的語句被當做命令式。即先執行代碼塊中的第一個語句,然后執行后面的語句,最后控制離開塊。
另一方面,迭代器塊不是需要在同一時間執行的一串命令式命令,而是描述了希望編譯器為我們創建的枚舉器類的行為。迭代器塊中的代碼描述了如何枚舉元素。
迭代器塊由兩個特殊語句:
- yield return語句指定了序列中返回的下一項
- yield break語句指定在序列中沒有的其他項
編譯器得到有關枚舉項的描述后,使用它來構建包含所有需要的方法和屬性實現的枚舉器類。結果類被嵌套包含在迭代器聲明的類中。
如下圖所示,根據迭代器塊的返回類型,你可以讓迭代器產生枚舉器或可枚舉類型。
使用迭代器來創建枚舉器
class MyClass { public IEnumerator<string> GetEnumerator() { return BlackAndWhite(); //返回枚舉器 } public IEnumerator<string> BlackAndWhite()//迭代器 { yield return "black"; yield return "gray"; yield return "white"; } } class Program { static void Main() { var mc=new MyClass(); foreach(string shade in mc) { Console.WriteLine(shade); } } }
下圖演示了MyClass的代碼及產生的對象。注意編譯器為我們自動做了多少工作。
- 圖左的迭代器代碼演示了它的返回類型是
IEnumerator<string>
- 圖右演示了它有一個嵌套類實現了
IEnumerator<string>
使用迭代器來創建可枚舉類型
之前示例創建的類包含兩部分:產生返回枚舉器方法的迭代器以及返回枚舉器的GetEnumerator方法。
本節例子中,我們用迭代器來創建可枚舉類型,而不是枚舉器。與之前的示例相比,本例有以下不同:
- 本例中,BlackAndWhite迭代器方法返回
IEnumerable<string>
而不是IEnumerator<string>
。因此MyClass首先調用BlackAndWhite方法獲取它的可枚舉類型對象,然后調用對象的GetEnumerator方法來獲取結果,從而實現GetEnumerator方法 - 在Main的foreach語句中,我們可以使用類的實例,也可以調用BlackAndWhite方法,因為它返回的是可枚舉類型
class MyClass { public IEnumerator<string> GetEnumerator() { IEnumerable<string> myEnumerable=BlackAndWhite(); return myEnumerable.GetEnumerator(); } public IEnumerable<string> BlackAndWhite()//迭代器 { yield return "black"; yield return "gray"; yield return "white"; } } class Program { static void Main() { var mc=new MyClass(); foreach(string shade in mc) { Console.Write(shade); } foreach(string shade in mc.BlackAndWhite) { Console.Write(shade); } } }
下圖演示了在代碼的可枚舉迭代器產生泛型可枚舉類型。
- 圖左的迭代器代碼演示了它的返回類型是
IEnumerable<string>
- 圖右演示了它有一個嵌套類實現了
IEnumerator<string>
和IEnumerable<string>
常見迭代器模式
前面兩節展示了,我們可以創建迭代器來返回可枚舉類型或枚舉器。下圖總結了如何使用普通迭代器模式。
- 當我們實現返回枚舉器的迭代器時,必須通過實現GetEnumerator來讓類可枚舉,如圖左
- 如果我們在類中實現迭代器返回可枚舉類型,我們可以讓類實現GetEnumerator來讓類本身可被枚舉,或不實現GetEnumerator,讓類不可枚舉
- 若實現GetEnumerator,讓它調用迭代器方法以獲取自動生成的實現IEnumerable的類實例。然后從IEnumerable對象返回由GetEnumerator創建的枚舉器,如圖右
- 若通過不實現GetEnumerator使類本身不可枚舉,仍然可以使用由迭代器返回的可枚舉類,只需要直接調用迭代器方法,如果右第二個foreach語句
產生多個可枚舉類型
下例中,Spectrum類有兩個可枚舉類型的迭代器。注意盡管它有兩個方法返回可枚舉類型,但類本身不是可枚舉類型,因為它沒有實現GetEnumerator
using System; using System.Collections.Generic; class Spectrum { string[] colors={"violet","blue","cyan","green","yellow","orange","red"}; public IEnumerable<string> UVtoIR() { for(int i=0;i<colors.Length;i++) { yield return colors[i]; } } public IEnumerable<string> IRtoUV() { for(int i=colors.Length-1;i>=0;i--) { yield return colors[i]; } } } class Program { static void Main() { var spectrum=new Spectrum(); foreach(string color in spectrum.UVtoIR()) { Console.Write(color); } Console.WriteLine(); foreach(string color in spectrum.IRtoUV()) { Console.Write(color); } Console.WriteLine(); } }
將迭代器作為屬性
本例演示兩個內容:第一,使用迭代器來產生具有兩個枚舉器的類;第二,演示迭代器如何實現屬性。
using System; using System.Collections.Generic; class Spectrum { bool _listFromUVtoIR; string[] colors={"violet","blue","cyan","green","yellow","orange","red"}; public Spectrum(bool listFromUVtoIR) { _listFromUVtoIR=listFromUVtoIR; } public IEnumerator<string> GetEnumerator() { return _listFromUVtoIR?UVtoIR:IRtoUV; } public IEnumera<string> UVtoIR { get { for(int i=0;i<colors.Length;i++) { yield return colors[i]; } } } public IEnumerable<string> IRtoUV { get { for(int i=colors.Length-1;i>=0;i--) { yield return colors[i]; } } } } class Program { static void Main() { var startUV=new Spectrum(true); var startIR=new Spectrum(false); foreach(string color in startUV) { Console.Write(color); } Console.WriteLine(); foreach(string color in startIR) { Console.Write(color); } Console.WriteLine(); } }
迭代器實質
如下是需要了解的有關迭代器的其他重要事項。
- 迭代器需要System.Collections.Generic命名空間
- 在編譯器生成的枚舉器中,Reset方法沒有實現。而它是接口需要的方法,因此調用時總是拋出System.NetSupportedException異常。
在后台,由編譯器生成的枚舉器類是包含4個狀態的狀態機。
- Before 首次調用MoveNext的初始狀態
- Running 調用MoveNext后進入這個狀態。在這個狀態中,枚舉器檢測並設置下一項的為知。在遇到yield return、yield break或在迭代器體結束時,退出狀態
- Suspended 狀態機等待下次調用MoveNext的狀態
- After 沒有更多項可以枚舉
如果狀態機在Before或Suspended狀態時調用MoveNext方法,就轉到了Running狀態。在Running狀態中,它檢測集合的下一項並設置為知。
如果有更多項,狀態機會轉入Suspended狀態,如果沒有更多項,它轉入並保持在After狀態。