天天看點

C# 中的枚舉器C# 中的枚舉器

C# 中的枚舉器

術語表

Iterator:枚舉器

如果你正在建立一個表現和行為都類似于集合的類,允許類的使用者使用foreach語句對集合中的成員進行枚舉将會是很友善的。這在C# 2.0中比 C# 1.1更容易實作一些。作為示範,我們先在 C# 1.1中為一個簡單的集合添加枚舉,然後我們修改這個範例,使用新的C#2.0 枚舉建構方法。

我們将以建立一個簡單化的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;

}

NOTE:實際開發中,通常使用ArrayList,而不是固定大小的數組。在這裡為了程式簡單就沒有做數組下标越界的檢測。

從感覺上看,ListBox像是一個集合,如果可以使用集合中通常使用的 foreach 循環來擷取listBox中的所有字元串将會是非常便利的。如此的話,可以這樣書寫代碼:

ListBox lb = new ListBox("a", "b", "c", "d", "e", "f", "g", "h");

foreach (string s in lb) {

    Console.WriteLine(s);

}

但是,會得到這樣一個錯誤:

“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字元串。 

繼續閱讀