Iterator:枚舉器(迭代器)
若是你正在建立一個表現和行爲都相似於集合的類,容許類的用戶使用foreach語句對集合中的成員進行枚舉將會是很方便的。這在C# 2.0中比 C# 1.1更容易實現一些。做爲演示,咱們先在 C# 1.1中爲一個簡單的集合添加枚舉,而後咱們修改這個範例,使用新的C#2.0 枚舉構建方法。html
咱們將以建立一個簡單化的List Box做爲開始,它將包含一個8字符串的數組和一個整型,這個整型用於記錄數組中已經添加了多少字符串。構造函數將對數組進行初始化並使用傳遞進來的參數填充它。數組
public ListBox(params string[] initialStrings)
{
strings = new String[8];
foreach (string s in initialStrings)
{
strings[ctr++] = s;
}
}安全
除此之外,ListBox類還須要一個Add方法(進行添加 string 的操做) 和 一個返回數組中字符串個數的方法。函數
public void Add(string theString)
{
strings[ctr] = theString;
ctr++;
}
public int GetNumEntries()
{
return ctr;
}post
NOTE:實際開發中,一般使用ArrayList,而不是固定大小的數組。在這裏爲了程序簡單就沒有作數組下標越界的檢測。.net
從感受上看,ListBox像是一個集合,若是可使用集合中一般使用的 foreach 循環來獲取listBox中的全部字符串將會是很是便利的。如此的話,能夠這樣書寫代碼:htm
ListBox lb = new ListBox("a", "b", "c", "d", "e", "f", "g", "h");
foreach (string s in lb) {
Console.WriteLine(s);
}對象
可是,會獲得這樣一個錯誤:blog
「Iterator.ListBox」不包含「GetEnumerator」的公共定義,所以 foreach 語句不能做用於「Iterator.ListBox」類型的變量索引
想要使用foreach語句,還必須實現IEnumerable 接口。
這個接口只要求實現一個方法: GetEnumerator。這個方法必須返回一個實現了IEnumerator 接口的對象。除此之外,咱們須要返回的這個對象不只實現了IEnumerator,並且知道如何枚舉ListBox對象。你將須要建立一個 ListBoxEmunerator(在下面描述):
NOTE: IEnumerable 和 IEnumerator 是不一樣的接口,請不要搞混了。
public IEnumerator GetEnumerator()
{
return new ListBoxEnumerator();
}
如今,ListBox 可使用 foreach 循環了:
ListBox lbt = new ListBox("Hello", "World");
lbt.Add("Who");
lbt.Add("Is");
lbt.Add("John");
lbt.Add("Galt");
foreach (string s in lbt)
{
Console.WriteLine("Value: {0}", s);
}
先是實例化這個ListBox ,並初始了兩個字符串,隨後又添加了四個。foreach循環接受ListBox實例,而且迭代它,依次返回字符串。輸出是:
Hello
World
Who
Is
John
Galt
實現 IEnumerator 接口
注意到ListBoxEnumerator不只須要實現IEnumerator接口,對於ListBox類它也須要一些特別瞭解;特別是,它必須能夠得到ListBox的字符串數組而且遍歷其所包含的字符串。IEnumerable 類和與其相關的 IEnumerator類之間的關係有一點微妙。實現IEnumerator接口的最好辦法是在IEnumerable類裏建立一個嵌套的IEnumerator類。
public class ListBox : IEnumerable
{
// 嵌套的私有ListBoxEnumerator類實現
private class ListBoxEnumerator : IEnumerator
{
// 代碼實現...
}
// ListBox類的代碼...
}
注意ListBoxEnumerator須要對它所嵌入的ListBox類的一個引用。你能夠經過ListBoxEnumerator的構造函數來傳遞。
爲了實現IEnumerator接口,ListBoxEnumerator須要兩個方法:MoveNext和Reset,還有一個屬性:Current。這些方法和屬性的任務是建立一個狀態機制,確保你能夠在任什麼時候候得知ListBox中的哪一個元素是當前元素,並得到那個元素。
在這個例子中,這種狀態機制是經過維護一個標明當前string的索引值來完成的,而且,你能夠經過對外部類的string集合進行索引來返回這個當前的string。爲了達到這個目標,你須要一個成員變量保存對於外部ListBox對象的引用,以及一個整型用於保存當前索引。
private ListBox lbt;
private int index;
每次Reset方法被調用的時候,index被置爲 -1。
public void Reset()
{
index = -1;
}
每次MoveNext被調用的時候,外部類的數組檢查時候已經到了末尾,若是是這樣,方法返回false。若是集合中還有對象,index將增長,而且方法返回true。
public bool MoveNext()
{
index++;
if (index >= lbt.strings.Length)
{
return false;
}else
{
return true;
}
}
最後,若是MoveNext方法返回True,foreach循環將調用Current屬性。ListBoxEnumerator的Current屬性的實現是索引外部類(ListBox)中的集合,而且返回找到的對象(這個例子中,是一個字符串)。注意,返回一個Object是由於IEnumerator接口中Current屬性的簽名如此。
public object Current
{
get {
return(lbt[index]);
}
}
在1.1中,全部想要經過foreach循環來迭代的類都須要實現IEnumerable接口,因而,必須建立一個實現了IEnumerator的類。最糟的是,enumerator返回的值並非類型安全的。記得Current屬性返回一個Object對象;它僅僅簡單的假設你所返回的值與foreach循環所指望的相符合。
C# 2.0 的解救辦法
使用C# 2.0 這些問題如同五月末的雪般融化了。在這個例子的2.0版本中,我重寫上面的列表,使用C# 2.0的兩個新特性:泛型 和 枚舉器。
我以從新定義實現IEumerable<string>的ListBox做爲開始:
public class ListBox : IEnumerable<string>
這樣作肯定這個類能夠在foreach循環中使用,同時確保迭代的值是string類型。
如今,從上個例子中挪去整個嵌套類,而且用下面的代碼替換 GetEnumerator方法。
public IEnumerator<string> GetEnumerator()
{
foreach (string s in strings)
{
yield return s;
}
}
GetEnumerator方法使用了新的 yield 語句。yield語句返回一個表達式。yield語句僅在迭代塊中出現,而且返回foreach語句所指望的值。那也就是,對GetEnumerator的每次調用都將會產生集合中的下一個字符串;全部的狀態管理已經都爲你作好了!
就這樣了,你已經完成了。不須要爲每一個類型實現你本身的enumerator,不須要建立嵌套類。你已經移除了至少30行代碼,而且極大地簡化了你的代碼。程序繼續像指望的那樣運行,可是狀態管理再也不是你的任務,全部的都爲你作好了。更進一步,由枚舉器所返回的值必定是string類型,若是你想要返回其餘類型,你能夠修改IEnumerable泛型語句,IEnumerable泛型語句將反射新類型。
關於Yield的更多內容
做爲對上一節的一些說明,應該告訴你:實際上,你能夠在yield語句塊中yield一個以上的值。這樣,下面的語句是徹底正確的C#語句:
public IEnumerator GetEnumerator()
{
yield return "Who";
yield return " is";
yield return "John Galt?";
}
假設上面的代碼位於一個名爲foo的類中,你能夠這樣寫:
foreach ( string s in new foo())
{
Console.Write(s);
}
輸出結果將會是:
Who is John Galt?
若是你如今停下來思考一下,這些也是以前的代碼所作的事。它遍歷了本身的foreach循環,而且產生出它所找到的每一個string字符串。