示例代碼:示例代碼_for_Csharp穩固基礎:傳統遍歷與迭代器 (下載)
Hello,Coders。我們除了天天的碼 if…else…之外,還會不斷的碼出foreach。我今天要說的是:傳統遍歷需實現的接口及我們還有一種更簡潔優雅的方式實現多種迭代器。
傳統遍歷
傳統的遍歷即通過讓集合類實現IEnumerable、IEnumerator或IEnumerable<T>、IEnumerator<T>接口來支持遍歷。
public interface IEnumerable // 可枚舉接口 { IEnumeratorGetEnumerator(); } public interface IEnumerator // 枚舉器接口 { object Current { get; } boolMoveNext(); void Reset(); }
1. 分析:
1) 從這兩個接口的用詞選擇上,也可以看出其不同:
a) IEnumerable是一個聲明式的接口,聲明實現該接口的類是可枚舉。
b) IEnumerator是一個實現式的接口,IEnumerator對象說明如何實現枚舉器。
2) Foreach語句隱式調用集合的無參GetEnumerator方法(不論集合是否有實現IEnumerable接口,但只要有無參GetEnumerator方法並返回IEnumerator就可遍歷)。
3) 集合類為什么不直接實現IEnumerable和IEnumerator接口?
這樣是為了提高並發性。Eg:一個遍歷機制只有一個Current,一旦並發就會出錯。然而“將遍歷機制與集合分離開來”如果要實現同時遍歷同一個集合,只需由集合IEnumerable.GetEnumerator() 返回一個新的包含遍歷機制(IEnumerator)的類實例即可。
1. 調用過程
插播一段:由foreach執行過程可知其迭代器是延遲計算的。
因為迭代的主體在MoveNext() 中實現,foreach中每次遍歷執行到 in 的時候才會調用MoveNext() ,所以其迭代器耗時的指令是延遲計算的。
延遲計算(Lazy evaluation):來源自函數式編程,在函數式編程里,將函數作為參數來傳遞,傳遞過程中不會執行函數內部耗時的計算,直到需要這個計算結果的時候才調用,這樣就可以因為避免一些不必要的計算而改進性能。 (另外還有linq、DataReader等也運用了延遲計算的思想)
1. 具體實現示例
/// <summary> /// 課程 /// </summary> public class Course { public Course(String name) { this.name = name; } private String name = string.Empty; public String Name { get { return name; } } } public class CourseCollection : IEnumerable<Course> { public CourseCollection() { arr_Course = new Course[] { new Course("語文"), new Course("數學"), new Course("英語"), new Course("體育") }; } private Course[] arr_Course; public Course this[int index] { get { return arr_Course[index]; } } public int Count { get { return arr_Course.Length; } } public IEnumerator<Course> GetEnumerator() { return new CourseEnumerator(this); } #region 實現 IEnumerable<T> private sealed class CourseEnumerator : IEnumerator<Course> { private readonly CourseCollection courseCollection; private int index; internal CourseEnumerator(CourseCollection courseCollection) { this.courseCollection = courseCollection; index = -1; } public Course Current { get { return courseCollection[index]; } } bool IEnumerator.MoveNext() { index++; return (index < courseCollection.Count); } void IEnumerator.Reset() { index = -1; } …… } #endregion …… }
有了對“傳統遍歷”實現方式的理解才能快速明白下一節“迭代器”的實現原理。要知道絕大部分最新的概念其實都可以用最簡單的那些概念組合而成。而只有對基本概念理解,才能看清那些復雜概念的實質。
迭代器(iterator)
迭代器是 C# 2.0 中的新功能。它使類或結構支持foreach迭代,而不必“顯示”實現IEnumerable或IEnumerator接口。只需要簡單的使用 yield 關鍵字,由 JIT 編譯器幫我們編譯成實現 IEnumerable或IEnumerator 接口的對象(即:本質還是傳統遍歷,只是寫法上非常簡潔)。
對於本節提到的編譯后的代碼,可通過 Reflector.exe , ILSpy.exe 進行查看。
1. 分析
1) yield 語句只能出現在 iterator 塊(迭代塊)中,該塊只能用作方法、運算符或get訪問器的主體實現。這類方法、運算符或訪問器的“主體”受以下約束的控制:
a) 不允許不安全塊。
b) 方法、運算符或訪問器的參數不能是 ref 或 out。
2) 迭代器代碼使用 yield return 語句依次返回每個元素。yield break 將終止迭代。
a) yield return 的時候會保存當前位置(狀態機)並把控制權從迭代器中交給調用的程序,做必要的返回值處理,下一次進入迭代器將從之前保存的位置處開始執行直到迭代結束或調用yield break。
b) yield break 就是控制權交給調用程序就不回來了從而終止迭代。
3) yield return 語句不能放在 try-catch 塊中。但可放在后跟 finally 塊的 try 塊中。
4) yield break 語句可放在 try 塊或 catch 塊中,但不能放在 finally 塊中。
5) yield 語句不能出現在匿名方法中。
6) 迭代器必須返回相同類型的值,因為最后輸出為IEnumerator.Current是單一類型。(見下面示例)
7) 在同一個迭代器中可以使用多個 yield 語句。(見下面示例)
8) 自定義迭代器:迭代器可以自定義名稱、可以帶參數,但在foreach中需要顯示去調用自定義的迭代器。(見下面示例)
9) 迭代器的返回類型必須為IEnumerator、IEnumerator<T>或IEnumerable、IEnumerable<T>。(見下面示例)
2. 迭代器的具體實現
1) 返回類型為IEnumerator、IEnumerator<T>
返回此類型的迭代器方法必須滿足:
a) 必須有GetEnumerator且不帶參數;
b) 必須是public公共成員;
見示例代碼,我們將CourseCollection集合對象的IEnumerable.GetEnumerator() 方法實現如下:
// 注意:返回什么,泛型就為什么類型 public IEnumerator<String> GetEnumerator() { for (int i = 0; i < arr_Course.Length; i++) { Course course = arr_Course[i]; yield return "選修:" + course.Name; // 兩個 yield return yield return Environment.NewLine; // 每個yield return 保存當前位置並返回值,下一次進入迭代器時將從之前保存的位置處開始執行 if (String.Compare(course.Name, "體育") == 0) yield break; List<string> strs=new List<string>{"435435","546546"}; foreach (string s in strs) { Console.WriteLine(s); } } }
經過 JIT 編譯后,會自動生成一個實現了 IEnumerator<String> 接口的對象。具體代碼可通過 Reflector 工具查看,下面展示其中的MoveNext() 代碼:
private bool MoveNext() { switch (this.<>1__state) { case 0: this.<>1__state = -1; this.<i>5__2 = 0; while (this.<i>5__2 < this.<>4__this.arr_Course.Length) { this.<course>5__3 = this.<>4__this.arr_Course[this.<i>5__2]; this.<>2__current = "選修:" + this.<course>5__3.Name; this.<>1__state = 1; return true; Label_007C: this.<>1__state = -1; this.<>2__current = Environment.NewLine; this.<>1__state = 2; return true; Label_009C: this.<>1__state = -1; if (string.Compare(this.<course>5__3.Name, "體育") == 0) { break; } this.<>g__initLocal0 = new List<string>(); this.<>g__initLocal0.Add("435435"); this.<>g__initLocal0.Add("546546"); this.<strs>5__4 = this.<>g__initLocal0; foreach (string s in this.<strs>5__4) { Console.WriteLine(s); } this.<i>5__2++; } break; case 1: goto Label_007C; case 2: goto Label_009C; } return false; }
通過代碼,我們可以知道:
a) 同一個迭代器中有多少個 yield return語句,while 循環中就有多少個 return true 。
b) yield retuen結束本次循環,yield break結束整個循環。輸出數據的順序通過生成類中的一個state狀態字段做為 switch 標識來決定要輸出第幾個 yield return 。yield return在每個case里面改變state內部字段,使正確執行完多個return返回數據,並最后通過return true來結束本次MoveNext()。而yield break語句直接生成break並重置state狀態字段為switch中沒有的值而跳出switch語句,通過執行最后的return false來結束整個循環。
c) 注意:yield return 后面的 List<string>代碼段也會被執行。
2) 返回類型為IEnumerable、IEnumerable<T>
返回此類型的迭代器必須滿足:
a) 必須可以在foreach語句中被調用(訪問權限);
返回此類型的迭代器通常用於實現自定義迭代器,即:迭代器可以自定義名稱、可以帶參數。Eg:(升序和降序)
public IEnumerable<String> GetEnumerable_ASC() { return this; } public IEnumerable<String> GetEnumerable_DESC() { for (inti = arr_Course.Length - 1; i>= 0; i--) { Course course = arr_Course[i]; yield return "選修:" +course.Name; yield return Environment.NewLine; } }
需如下進行迭代器調用:
yield_Example.CourseCollection col2 = new yield_Example.CourseCollection(); foreach (String str in col2.GetEnumerable_ASC()){ // col2.GetEnumerable_ASC()} foreach (String str in col2.GetEnumerable_DESC()){//col2.GetEnumerable_DESC()}
經過 JIT 編譯后,會自動生成一個直接實現IEnumerator<String>和IEnumerator<String>接口的對象,其GetEnumerator() 方法返回自己this(因為本身實現了IEnumerator接口)。
這是因為在不同foreach遍歷中所訪問的由編譯器自動生成的迭代器具有其自己獨立的狀態,所以迭代器之間互不影響,不存在並發的問題。
OVER,謝謝觀看。(如有缺漏或錯誤請留言,謝謝!!!)