作者 | 喵叔
責編 | 胡巍巍
出品 | 程序人生(ID:coder_life)
今天來講解一下開發人員會用但不理解的C#中的知識,這篇文章我們講解一下枚舉器與迭代器的知識。
1.枚舉器
什么是枚舉器?
枚舉器(enumerator)是一個只讀的作用于序列值的只能向前的游標,并且實現了System.Collections.IEnumeratar 或者 System.Collections.Generic.IEnumeratar<T>接口的對象。
通常來說任何一個包含名為MoveNext方法和名稱為Current屬性的對象,.NET都會將它作為枚舉器對待。通過一個例子來看一下枚舉器是怎么定義的:
class EnumeratorDemo : IEnumerator { public object Current { get { return true; } } public bool MoveNext { return false; } public void Reset { //more code } }
可枚舉對象(enumerator object)是一序列的邏輯表示,本身不是游標,但可以基于本身產生游標對象。如果要迭代可枚舉對象,可以使用 foreach 語句。
可枚舉對象可以是實現了Ienumerable或Ienumerable<T>的對象,也可以是具有名為GetEnumerator方法并且方法返回一個枚舉器的對象。同樣我們通過代碼來看一下怎么定義可枚舉對象:
class Enumerable : IEnumerable { public IEnumerator GetEnumerator { IEnumerable<string> myEnumerable = ; return myEnumerable.GetEnumerator; } }
上述代碼只是為了展現枚舉器和可枚舉對象的定義,實際開發過程中應該需要將對應的方法填充完成。下面我們通過實際代碼來看一下枚舉器和可枚舉對象在開發中的使用:
static void Main(string[] args) { using (var item = "abcdefg".GetEnumerator) { while (item.MoveNext) { var _char = item.Current; Console.WriteLine(_char); } } Console.Read; }
講解一下上述代碼,字符串是可枚舉對象,因此可以通過 GetEnumerator方法獲得枚舉器,然后使用枚舉器的MoveNext防火閥做 while語句的執行條件,MoveNext法在存在下一個值得時候會返回True,當不存在的時候返回False。
最后使用枚舉器的Current屬性獲得游標指向的值,并打印出來。打印結果如下:

如果枚舉器實現了IDisposable接口,這時foreach語句可以隱式的清理掉枚舉對象。
小知識
我們在C#中可以用一行代碼完成可枚舉對象的實例化和元素填充,這種方法叫做集合初始化器,代碼如下:
List<string> strList = new List<string> { "張三", "李四", "王五", "趙六" };
上述代碼會被編譯器翻譯成如下代碼:
List<string> strList = new List<string>; strList.Add("張三"); strList.Add("李四"); strList.Add("王五"); strList.Add("趙六");
在前面的例子中要求可枚舉對象實現了System.Collections.IEnumerable接口,并存在一個可以接受適當參數的Add方法。同理字典也可以通過集合初始化器進行對象初始化和元素填充。
2.迭代器
什么是迭代器?
迭代器簡化了對象間的通信,使得不關心序列的類型,而獲得序列中的每個元素。C#中迭代器被IEnumerator和IEnumerable和其對應的泛型接口封裝。
一個類如果實現了IEnumerable接口,那么就能夠被迭代;調用GetEnumerator方法將返回IEnumerator接口的實現,它就是迭代器本身。下面我們先通過例子來看一下迭代器的使用:
static void Main(string[] args) { foreach (int item in demo(5)) { Console.WriteLine(item); } Console.Read; } static IEnumerable<int> demo(int demoCount) { int data = 0; for (int i = 0; i < demoCount; i++) { yield return data; data = data + 1; } }
上述代碼中用到了yield return , yield return語句的意思是請求從枚舉器產生的下一個元素。
每當遇到yield時控制權都會回歸到調用者那里,但是被調用者的狀態還會保持。這個狀態的生命周期綁定到了枚舉器上,當調用這完成枚舉之后狀態就被釋放。
3.原理
編譯器把地帶方法轉換成私有的,實現了 IEnumerable<T>或者 IEnumerator<T>的類。內部的邏輯被反轉并被切分到編譯器生成的枚舉器類中的 MoveNext方法和 Current屬性里。這就意味著當你調用迭代器方法時,實際上時對編譯器生成的類進行實例化。
4.語義
迭代器含有一個或多個 yield語句的方法、屬性或者索引器。而且必須返回 IEnumerable、IEnumerable<T>、 IEnumerator或者 IEnumerator<T>其中的一個,迭代器會根據返回的接口類型選擇不同的語義。下面根據上述描述來看一個例子:
static void Main { foreach(int item in demo) { Console.WriteLine(item); } } static IEnumerable<int> Foo { yield return 1; yield return 2; yield return 3; }
迭代器中的return
再迭代器中我們無法使用 return 跳出迭代器,只能使用 yield break來跳出迭代器。我們來看一段代碼:
static IEnumerable<int> demo { yield return 1; yield return 2; yield return 3; yield break; yield return 4; }
上述代碼中我們使用了 yield break來跳出迭代器,yield break 后面的 yield return 4將不再返回。
這里需要注意的是 yield return 語句不可以出現在 try...catch...finally 語句塊中。
但是如果沒有 catch 語句塊,只有 try..finally 語句塊,則 yield return 可以出現在其中。
在 try..finally 語句塊中,當枚舉器到達終點時或者被釋放掉時,finally 語句塊中的代碼就會執行,如果執行了 yield return 那么 foreach 語句也會釋放掉枚舉器,然后執行 finally 中的代碼。
當我們顯示使用枚舉器時如果沒有釋放掉枚舉,那么將不會執行 finally 中的代碼,為了避免這種情況的出現我們可以使用 using 語句。
迭代器我們可以嵌套使用,我們看一下例子:
class Program { static void Main(string[] args) { foreach (int fib in GetNum(Fibs(12))) Console.WriteLine(fib); Console.Read; } static IEnumerable<int> Fibs(int count) { int result = 0; for (int i = 0; i < count; i++) { yield return result; result = result + i; } } static IEnumerable<int> GetNum(IEnumerable<int> sequence) { foreach (int x in sequence) { yield return x; } } }
上述例子中每個元素在 MoveNext 操作請求的時候才會被計算,也就是說 foreach (int fib in GetNum(Fibs(12)))沒執行一次循環每個元素值就會被計算一次。
這里需要注意,一般來說迭代器都會結合 foreach語句一起使用,每次循環完成后都必須顯示的或隱式的調用 Dispose方法來釋放掉枚舉器。
這篇文章基本上涵蓋了迭代器和枚舉器的所有內容,如果需要進一步學習迭代器與枚舉器,需要自己動手實踐一下。
作者簡介:朱鋼,筆名喵叔,CSDN博客專家,.NET高級開發工程師,7年一線開發經驗,參與過電子政務系統和AI客服系統的開發,以及互聯網招聘網站的架構設計,目前就職于北京恒創融慧科技發展有限公司,從事企業級安全監控系統的開發。