迭代器實現
編譯器通過產生的嵌套類來維護迭代狀態。當在foreach迴圈中(或在直接的迭代代碼中)首次調用迭代器時,編譯器為GetEnumerator函數產生的編譯產生(Compiler-Generated)代碼將建立一個帶有reset狀態的新的迭代器對象(即嵌套類的一個執行個體)。在foreach每次迴圈調用迭代器的MoveNext方法時,它都從前一次yield return語句停止的地方開始執行。只要foreach迴圈執行,迭代器就會維持它的狀態。然而,迭代器對象(以及它的狀態)在多個foreach迴圈之間並不保持一致。因此,再次調用foreach是安全的,因為將產生新的迭代器對象並開始新的迭代。這就是為什麼IEnumerable<ItemType>沒有定義Reset方法的原因。
但是嵌套迭代器類是如何?的呢?並且如何管理它的狀態呢?編譯器將一個標準方法轉換成一個可以被多次調用的方法,此方法使用一個簡單的狀態機器在前一個yield return語句之後恢複執行。開發人員需要做的只是使用yield return語句指示編譯器產生什麼以及何時產生。編譯器具有足夠的智能,它甚至能夠將多個yield return語句按照它們出現的順序串連起來:
public class CityCollection : IEnumerable
{
public IEnumerator GetEnumerator()
{
yield return "New York";
yield return "Paris";
yield return "London";
}
}
讓我們看一看在下面幾行代碼中顯示的該類的GetEnumerator方法:
public class MyCollection : IEnumerable
{
public IEnumerator GetEnumerator()
{
//Some iteration code that uses yield return
}
}
當編譯器遇到這種帶有yield return語句的類成員時,它會插入一個名為GetEnumerator$<random unique number>__IEnumeratorImpl的嵌套類的定義,5中C#虛擬碼所示。(記住,本文所討論的所有特徵,包括編譯器產生的類和欄位的名稱是會改變的,在某些情況下甚至會發生徹底的變化。您不應該試圖使用反射來獲得這些實現細節並期望得到一致的結果。)
public class MyCollection : IEnumerable<string>
{
public virtual IEnumerator<string> GetEnumerator()
{
GetEnumerator$0003__IEnumeratorImpl impl;
impl = new GetEnumerator$0003__IEnumeratorImpl;
impl.<this> = this;
return impl;
}
private class GetEnumerator$0003__IEnumeratorImpl :
IEnumerator<string>
{
public MyCollection <this>; // Back reference to the collection
string $_current;
// state machine members go here
string IEnumerator<string>.Current
{
get
{
return $_current;
}
}
bool IEnumerator<string>.MoveNext()
{
//State machine management
}
IDisposable.Dispose()
{
//State machine cleanup if required
}
}
}
圖5編譯器產生的迭代程式
嵌套類實現了從類成員返回的相同IEnumerable介面。編譯器使用一個執行個體化的巢狀型別來代替類成員中的代碼,將一個指向集合的引用賦給嵌套類的this成員變數,類似於圖2中所示的手動實現。實際上,該嵌套類是一個實現IEnumerator介面的類。
遞迴迭代
當在像二叉樹或包含相互連通節點的圖這樣的資料結構上進行遞迴迭代時,迭代器才真正顯示出了它的優勢。手工實現一個遞迴迭代的迭代器是相當困難的,但是如果使用C#迭代器,就很容易。請考慮圖6中的二叉樹。本文所提供的原始碼包含了此二叉樹的完整實現。
class Node<T>
{
public Node<T> LeftNode;
public Node<T> RightNode;
public T Item;
}
public class BinaryTree<T>
{
Node<T> m_Root;
public void Add(params T[] items)
{
foreach(T item in items)
Add(item);
}
public void Add(T item)
{}
public IEnumerable<T> InOrder
{
get
{
return ScanInOrder(m_Root);
}
}
IEnumerable<T> ScanInOrder(Node<T> root)
{
if(root.LeftNode != null)
{
foreach(T item in ScanInOrder(root.LeftNode))
{
yield return item;
}
}
yield return root.Item;
if(root.RightNode != null)
{
foreach(T item in ScanInOrder(root.RightNode))
{
yield return item;
}
}
}
}
圖6實現遞迴迭代
這個二叉樹在節點中儲存了一些項。每個節點均擁有一個類型T(名為Item)的值。每個節點均含有指向左邊節點的引用和指向右邊節點的引用。比Item小的值儲存在左邊的子樹中,比Item大的值儲存在右邊的子樹中。這個樹還提供了Add方法,通過使用參數限定符添加一組的T類型的值:
public void Add(params T[] items);
這棵樹提供了一個IEnumerable<T>類型的名為InOrder的公用屬性。InOrder調用私人的輔助遞迴函式ScanInOrder並把樹的根節點傳遞給ScanInOrder。ScanInOrder定義如下:
IEnumerable ScanInOrder(Node root);
它返回IEnumerable<T>類型的迭代器的實現,此實現按順序遍曆二叉樹。對於ScanInOrder需要注意的一件事情是,它通過遞迴遍曆這個二叉樹的方式,即使用foreach迴圈來訪問從遞迴調用返回的IEnumerable<T>實現。在順序(in-order)迭代中,每個節點都首先遍曆它左邊的子樹,接著遍曆該節點本身的值,然後遍曆右邊的子樹。對於這種情況,需要三個yield return語句。為了遍曆左邊的子樹,ScanInOrder在遞迴調用(它以參數的形式傳遞左邊的節點)返回的IEnumerable<T>上使用foreach迴圈。一旦foreach迴圈返回,就已經遍曆左邊子樹的所有節點。然後,ScanInOrder產生作為迭代的根傳遞給其節點的值,並在foreach迴圈中執行另一個遞迴調用,這次是在右邊的子樹上。
通過使用屬性InOrder,可以編寫下面的foreach迴圈來遍曆整個樹:
BinaryTree tree = new BinaryTree();
tree.Add(4,6,2,7,5,3,1);
foreach(int num in tree.InOrder)
{
Trace.WriteLine(num);
}
// Traces 1,2,3,4,5,6,7
可以通過添加其他的屬性用相似的方式實現前序(pre-order)和後序(post-order)迭代。雖然以遞迴方式使用迭代器的能力顯然是一個強大的功能,但是在使用時應該保持謹慎,因為可能會出現嚴重的效能問題。每次調用ScanInOrder都需要執行個體化編譯器產生的迭代器,因此,遞迴遍曆一個很深的樹可能會導致在幕後產生大量的對象。在對稱二叉樹中,大約有n個迭代器執行個體,其中n為樹中節點的數目。在任一特定的時刻,這些對象中大約有log(n)個是活的。在具有適當大小的樹中,許多這樣的對象會使樹通過0代(Generation 0)記憶體回收。也就是說,通過使用棧或隊列維護一列將要被檢查的節點,迭代器仍然能夠方便地遍曆遞迴資料結構(例如樹)。