C# 中的枚舉器
出處:http://www.ondotnet.com/pub/a/dotnet/2004/06/07/liberty.html
術(shù)語(yǔ)表
Iterator:枚舉器
如果你正在創(chuàng )建一個(gè)表現和行為都類(lèi)似于集合的類(lèi),允許類(lèi)的用戶(hù)使用foreach語(yǔ)句對集合中的成員進(jìn)行枚舉將會(huì )是很方便的。這在C# 2.0中比 C# 1.1更容易實(shí)現一些。作為演示,我們先在 C# 1.1中為一個(gè)簡(jiǎn)單的集合添加枚舉,然后我們修改這個(gè)范例,使用新的C#2.0 枚舉構建方法。
我們將以創(chuàng )建一個(gè)簡(jiǎn)單化的List Box作為開(kāi)始,它將包含一個(gè)8字符串的數組和一個(gè)整型,這個(gè)整型用于記錄數組中已經(jīng)添加了多少字符串。構造函數將對數組進(jìn)行初始化并使用傳遞進(jìn)來(lái)的參數填充它。
public ListBox(params string[] initialStrings)
{
strings = new String[8];
foreach (string s in initialStrings)
{
strings[ctr++] = s;
}
}
除此以外,ListBox類(lèi)還需要一個(gè)Add方法(進(jìn)行添加 string 的操作) 和 一個(gè)返回數組中字符串個(gè)數的方法。
public void Add(string theString)
{
strings[ctr] = theString;
ctr++;
}
public int GetNumEntries()
{
return ctr;
}
NOTE:實(shí)際開(kāi)發(fā)中,通常使用ArrayList,而不是固定大小的數組。在這里為了程序簡(jiǎn)單就沒(méi)有做數組下標越界的檢測。
從感覺(jué)上看,ListBox像是一個(gè)集合,如果可以使用集合中通常使用的 foreach 循環(huán)來(lái)獲取listBox中的所有字符串將會(huì )是非常便利的。如此的話(huà),可以這樣書(shū)寫(xiě)代碼:
ListBox lb = new ListBox("a", "b", "c", "d", "e", "f", "g", "h");
foreach (string s in lb) {
Console.WriteLine(s);
}
但是,會(huì )得到這樣一個(gè)錯誤:
“Iterator.ListBox”不包含“GetEnumerator”的公共定義,因此 foreach 語(yǔ)句不能作用于“Iterator.ListBox”類(lèi)型的變量
想要使用foreach語(yǔ)句,還必須實(shí)現IEnumerable 接口。
這個(gè)接口只要求實(shí)現一個(gè)方法: GetEnumerator。這個(gè)方法必須返回一個(gè)實(shí)現了IEnumerator 接口的對象。除此以外,我們需要返回的這個(gè)對象不僅實(shí)現了IEnumerator,而且知道如何枚舉ListBox對象。你將需要創(chuàng )建一個(gè) ListBoxEmunerator(在下面描述):
NOTE: IEnumerable 和 IEnumerator 是不同的接口,請不要搞混了。
public IEnumerator GetEnumerator()
{
return new ListBoxEnumerator();
}
現在,ListBox 可以使用 foreach 循環(huán)了:
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);
}
先是實(shí)例化這個(gè)ListBox ,并初始了兩個(gè)字符串,隨后又添加了四個(gè)。foreach循環(huán)接受ListBox實(shí)例,并且迭代它,依次返回字符串。輸出是:
Hello
World
Who
Is
John
Galt
實(shí)現 IEnumerator 接口
注意到ListBoxEnumerator不僅需要實(shí)現IEnumerator接口,對于ListBox類(lèi)它也需要一些特別了解;特別是,它必須可以獲得ListBox的字符串數組并且遍歷其所包含的字符串。IEnumerable 類(lèi)和與其相關(guān)的 IEnumerator類(lèi)之間的關(guān)系有一點(diǎn)微妙。實(shí)現IEnumerator接口的最好辦法是在IEnumerable類(lèi)里創(chuàng )建一個(gè)嵌套的IEnumerator類(lèi)。
public class ListBox : IEnumerable
{
// 嵌套的私有ListBoxEnumerator類(lèi)實(shí)現
private class ListBoxEnumerator : IEnumerator
{
// 代碼實(shí)現...
}
// ListBox類(lèi)的代碼...
}
注意ListBoxEnumerator需要對它所嵌入的ListBox類(lèi)的一個(gè)引用。你可以通過(guò)ListBoxEnumerator的構造函數來(lái)傳遞。
為了實(shí)現IEnumerator接口,ListBoxEnumerator需要兩個(gè)方法:MoveNext和Reset,還有一個(gè)屬性:Current。這些方法和屬性的任務(wù)是創(chuàng )建一個(gè)狀態(tài)機制,確保你可以在任何時(shí)候得知ListBox中的哪個(gè)元素是當前元素,并獲得那個(gè)元素。
在這個(gè)例子中,這種狀態(tài)機制是通過(guò)維護一個(gè)標明當前string的索引值來(lái)完成的,并且,你可以通過(guò)對外部類(lèi)的string集合進(jìn)行索引來(lái)返回這個(gè)當前的string。為了達到這個(gè)目標,你需要一個(gè)成員變量保存對于外部ListBox對象的引用,以及一個(gè)整型用于保存當前索引。
private ListBox lbt;
private int index;
每次Reset方法被調用的時(shí)候,index被置為 -1。
public void Reset()
{
index = -1;
}
每次MoveNext被調用的時(shí)候,外部類(lèi)的數組檢查時(shí)候已經(jīng)到了末尾,如果是這樣,方法返回false。如果集合中還有對象,index將增加,并且方法返回true。
public bool MoveNext()
{
index++;
if (index >= lbt.strings.Length)
{
return false;
}else
{
return true;
}
}
最后,如果MoveNext方法返回True,foreach循環(huán)將調用Current屬性。ListBoxEnumerator的Current屬性的實(shí)現是索引外部類(lèi)(ListBox)中的集合,并且返回找到的對象(這個(gè)例子中,是一個(gè)字符串)。注意,返回一個(gè)Object是因為IEnumerator接口中Current屬性的簽名如此。
public object Current
{
get {
return(lbt[index]);
}
}
在1.1中,所有想要通過(guò)foreach循環(huán)來(lái)迭代的類(lèi)都需要實(shí)現IEnumerable接口,于是,必須創(chuàng )建一個(gè)實(shí)現了IEnumerator的類(lèi)。最糟的是,enumerator返回的值并不是類(lèi)型安全的。記得Current屬性返回一個(gè)Object對象;它僅僅簡(jiǎn)單的假設你所返回的值與foreach循環(huán)所期望的相符合。
C# 2.0 的解救辦法
使用C# 2.0 這些問(wèn)題如同五月末的雪般融化了。在這個(gè)例子的2.0版本中,我重寫(xiě)上面的列表,使用C# 2.0的兩個(gè)新特性:泛型 和 枚舉器。
我以重新定義實(shí)現IEumerable<string>的ListBox作為開(kāi)始:
public class ListBox : IEnumerable<string>
這樣做確定這個(gè)類(lèi)可以在foreach循環(huán)中使用,同時(shí)確保迭代的值是string類(lèi)型。
現在,從上個(gè)例子中挪去整個(gè)嵌套類(lèi),并且用下面的代碼替換 GetEnumerator方法。
public IEnumerator<string> GetEnumerator()
{
foreach (string s in strings)
{
yield return s;
}
}
GetEnumerator方法使用了新的 yield 語(yǔ)句。yield語(yǔ)句返回一個(gè)表達式。yield語(yǔ)句僅在迭代塊中出現,并且返回foreach語(yǔ)句所期望的值。那也就是,對GetEnumerator的每次調用都將會(huì )產(chǎn)生集合中的下一個(gè)字符串;所有的狀態(tài)管理已經(jīng)都為你做好了!
就這樣了,你已經(jīng)完成了。不需要為每個(gè)類(lèi)型實(shí)現你自己的enumerator,不需要創(chuàng )建嵌套類(lèi)。你已經(jīng)移除了至少30行代碼,并且極大地簡(jiǎn)化了你的代碼。程序繼續像期望的那樣運行,但是狀態(tài)管理不再是你的任務(wù),所有的都為你做好了。更進(jìn)一步,由枚舉器所返回的值一定是string類(lèi)型,如果你想要返回其他類(lèi)型,你可以修改IEnumerable泛型語(yǔ)句,IEnumerable泛型語(yǔ)句將反射新類(lèi)型。
關(guān)于Yield的更多內容
作為對上一節的一些說(shuō)明,應該告訴你:實(shí)際上,你可以在yield語(yǔ)句塊中yield一個(gè)以上的值。這樣,下面的語(yǔ)句是完全正確的C#語(yǔ)句:
public IEnumerator GetEnumerator()
{
yield return "Who";
yield return " is";
yield return "John Galt?";
}
假設上面的代碼位于一個(gè)名為foo的類(lèi)中,你可以這樣寫(xiě):
foreach ( string s in new foo())
{
Console.Write(s);
}
輸出結果將會(huì )是:
Who is John Galt?
如果你現在停下來(lái)思考一下,這些也是之前的代碼所做的事。它遍歷了自己的foreach循環(huán),并且產(chǎn)生出它所找到的每個(gè)string字符串。

