匹夫细说C#:庖丁解牛迭代器,那些藏在幕后的秘密
匹夫細說C#:庖丁解牛迭代器,那些藏在幕后的秘密
c#語言規范
閱讀目錄
- 0x00 前言
- 0x01 你好,迭代器
- 0x02 原來是狀態機呀
- 0x03 狀態管理
- 0x04 總結
0x00 前言
在匹夫的上一篇文章《匹夫細說C#:不是“棧類型”的值類型,從生命周期聊存儲位置》的最后,匹夫以總結和后記的方式涉及到一部分迭代器的知識。但是覺得還是不夠過癮,很多需要說清楚的內容還是含糊不清,所以這周就專門寫一下c#中的迭代器吧。
回到目錄0x01 你好,迭代器
首先思考一下,在什么情景下我們需要使用到迭代器?
假設我們有一個數據容器(可能是Array,List,Tree等等),對我們這些使用者來說,我們顯然希望這個數據容器能提供一種無需了解它的內部實現就可以獲取其元素的方法,無論它是Array還是List或者別的什么,我們希望可以通過相同的方法達到我們的目的。
此時,迭代器模式(iterator pattern)便應運而生,它通過持有迭代狀態,追蹤當前元素并且識別下一個需要被迭代的元素,從而可以讓使用者透過特定的界面巡訪容器中的每一個元素而不用了解底層的實現。
那么,在c#中,迭代器到底是以一個怎樣的面目出現的呢?
如我們所知,它們被封裝在IEnumerable和IEnumerator這兩個接口中(當然,還有它們的泛型形式,要注意的是泛型形式顯然是強類型的。且IEnumerator<T>實現了IDisposable接口)。
IEnumerable非泛型形式:
//IEnumerable非泛型形式[ComVisibleAttribute(True)] [GuidAttribute("496B0ABE-CDEE-11d3-88E8-00902754C43A")] public interface IEnumerable {IEnumerator GetEnumerator(); }IEnumerator非泛型形式:
//IEnumerator非泛型形式 [ComVisibleAttribute(true)] [GuidAttribute("496B0ABF-CDEE-11d3-88E8-00902754C43A")] public interface IEnumerator {Object Current {get;}bool MoveNext();void Reset(); }IEnumerable泛型形式:
//IEnumerable泛型形式public interface IEnumerable<out T> : IEnumerable {IEnumerator<T> GetEnumerator();IEnumerator GetEnumerator(); }IEnumerator泛型形式:
//IEnumerator泛型形式public interface IEnumerator<out T> : IDisposable, IEnumerator {void Dispose(); Object Current {get;} T Current {get;}bool MoveNext(); void Reset(); }[ComVisibleAttribute(true)] public interface IDisposable {void Dispose(); }IEnumerable接口定義了一個可以獲取IEnumerator的方法——GetEnumerator()。
而IEnumerator則在目標序列上實現循環迭代(使用MoveNext()方法,以及Current屬性來實現),直到你不再需要任何數據或者沒有數據可以被返回。使用這個接口,可以保證我們能夠實現常見的foreach循環。
為什么會有2個接口?
到此,各位看官是否和曾經的匹夫有相同的疑惑呢?那就是為何IEnumerable自己不直接實現MoveNext()方法、提供Current屬性呢?為何還需要額外的一個接口IEnumerator來專門做這個工作?
OK,假設有兩個不同的迭代器要對同一個序列進行迭代。當然,這種情況很常見,比如我們使用兩個嵌套的foreach語句。我們自然希望兩者相安無事,不要互相影響彼此。所以自然而然的,我們需要保證這兩個獨立的迭代狀態能夠被正確的保存、處理。這也正是IEnumerator要做的工作。而為了不違背單一職責原則,不使IEnumerable擁有過多職責從而陷入分工不明的窘境,所以IEnumerable自己并沒有實現MoveNext()方法。
迭代器的執行步驟
為了更直觀的了解一個迭代器,匹夫這里提供一個小例子。
using System; using System.Collections.Generic;class Class1 { static void Main(){foreach (string s in GetEnumerableTest()){Console.WriteLine(s);}}static IEnumerable<string> GetEnumerableTest(){yield return "begin";for (int i=0; i < 10; i++){yield return i.ToString();}yield return "end";} }輸出結果如圖:
OK,那么匹夫就給各位捋一下這段代碼的執行過程。
這個例子中迭代器的執行過程,匹夫已經給各位看官簡單的描述了一下。但是還有幾點需要關注的,匹夫也想提醒各位注意一下。
- 在第一次調用MoveNext()方法之前,我們自己在GetEnumerableTest中的代碼不會執行
- 之后調用MoveNext()方法時,會從上次暫停(yield return)的地方開始。
- 編譯器會保證GetEnumerableTest方法中的局部變量能夠被保留,換句話說,雖然本例中的i是值類型實例,但是它的值其實是被迭代器保存在堆上的,這樣才能保證每次調用MoveNext時,它是可用的。這也是匹夫上一篇文章中說迭代器塊中的局部變量會被分配在堆上的原因。
好啦,簡單總結了一下C#中的迭代器的外觀。那么接下來,我們繼續向內部前進,來看看迭代器究竟是如何實現的。
回到目錄0x02 原來是狀態機呀
上一節我們已經從外部看到了IEnumerable和IEnumerator這兩個接口的用法了,但是它們的內部到底是如何實現的呢?兩者之間又有何區別呢?
既然要深入迭代器的內部,這就是一個不得不面對的問題。
那么匹夫就寫一個小程序,之后再通過反編譯的方式,看看在我們自己手動寫的代碼背后,編譯器究竟又給我們做了哪些工作吧。
為了簡便起見,這個小程序僅僅實現一個按順序返回0-9這10個數字的功能。
IEnumerator的內部實現
首先,我們定義一個返回IEnumerator<T>的方法TestIterator()。
//IEnumerator<T>測試using System; using System.Collections;class Test {static IEnumerator<int> TestIterator(){for (int i = 0; i < 10; i++){yield return i;}} }接下來,我們看看反編譯之后的代碼,探查一下編譯器到底為我們做了什么吧。
internal class Test {// Methods 注,此時還沒有執行任何我們寫的代碼private static IEnumerator<int> TestIterator(){return new <TestIterator>d__0(0);}// Nested Types 編譯器生成的類,用來實現迭代器。 [CompilerGenerated]private sealed class <TestIterator>d__0 : IEnumerator<int>, IEnumerator, IDisposable{// Fields 字段:state和current是默認出現的private int <>1__state;private int <>2__current;public int <i>5__1;//<i>5__1來自我們迭代器塊中的局部變量,匹夫上一篇文章中提到過// Methods 構造函數,初始化狀態 [DebuggerHidden]public <TestIterator>d__0(int <>1__state){this.<>1__state = <>1__state;}// 幾乎所有的邏輯在這里private bool MoveNext(){switch (this.<>1__state){case 0:this.<>1__state = -1;this.<i>5__1 = 0;while (this.<i>5__1 < 10){this.<>2__current = this.<i>5__1;this.<>1__state = 1;return true;Label_0046:this.<>1__state = -1;this.<i>5__1++;}break;case 1:goto Label_0046;}return false;}[DebuggerHidden]void IEnumerator.Reset(){throw new NotSupportedException();}void IDisposable.Dispose(){}// Propertiesint IEnumerator<int>.Current{[DebuggerHidden]get{return this.<>2__current;}}object IEnumerator.Current{[DebuggerHidden]get{return this.<>2__current;}}} }我們先全面的看一下反編譯之后的代碼,可以發現幾乎所有的邏輯都發生在MoveNext()方法中。那么之后我們再詳細介紹下它,現在我們先從上到下把代碼捋一遍。
OK,IEnumerator接口我們看完了。下面再來看看另一個接口IEnumerable吧。
IEnumerator VS IEnumerable
依樣畫葫蘆,這次我們仍然是寫一個實現按順序返回0-9這10個數字的功能的小程序,只不過返回類型變為IEnumerable<T>。
using System; using System.Collections.Generic;class Test {static IEnumerable<int> TestIterator(){for (int i = 0; i < 10; i++){yield return i;}} }之后,我們同樣通過反編譯,看看編譯器又背著我們做了什么。
internal class Test {private static IEnumerable<int> TestIterator(){return new <TestIterator>d__0(-2);}private sealed class <TestIterator>d__0 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable{// Fieldsprivate int <>1__state;private int <>2__current;private int <>l__initialThreadId;public int <count>5__1;public <TestIterator>d__0(int <>1__state){this.<>1__state = <>1__state;this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId;}private bool MoveNext(){switch (this.<>1__state){case 0:this.<>1__state = -1;this.<count>5__1 = 0;while (this.<count>5__1 < 10){this.<>2__current = this.<count>5__1;this.<>1__state = 1;return true;Label_0046:this.<>1__state = -1;this.<count>5__1++;}break;case 1:goto Label_0046;}return false;}IEnumerator<int> IEnumerable<int>.GetEnumerator(){if ((Thread.CurrentThread.ManagedThreadId == this.<>l__initialThreadId) && (this.<>1__state == -2)){this.<>1__state = 0;return this;}return new Test.<TestIterator>d__0(0);}IEnumerator IEnumerable.GetEnumerator(){return ((IEnumerable<Int32>) this).GetEnumerator();}void IEnumerator.Reset(){throw new NotSupportedException();}void IDisposable.Dispose(){}int IEnumerator<int>.Current{get{return this.<>2__current;}}object IEnumerator.Current{get{return this.<>2__current;}}} }看到反編譯出的代碼,我們就很容易能對比出區別。
所以,從這些對比中我們能發現些什么嗎?思考一下我們經常使用的一些用法,包括匹夫在上一節中提供的小例子。不錯,我們會創建一個IEnumerable<T>的實例,之后一些語句(例如foreach)會去調用GetEnumerator方法獲取一個Enumerator<T>的實例,之后迭代數據,最終結束后釋放掉迭代器的實例(這一步foreach會幫我們做)。(而最初我們得到的IEnumerable<T>實例,在第一次調用GetEnumerator方法獲得了一個Enumerator<T>實例之后就再沒有用到了。)
而分析IEnumerable的GetEnumerator方法:
IEnumerator<int> IEnumerable<int>.GetEnumerator(){if ((Thread.CurrentThread.ManagedThreadId == this.<>l__initialThreadId) && (this.<>1__state == -2)){this.<>1__state = 0;return this;}return new Test.<TestIterator>d__0(0);}我們可以發現,-2這個狀態,也就是此時的初始狀態,表明了GetEnumerator()方法還沒有執行。而0這個狀態,則表明已經準備好了迭代,但是MoveNext()尚未調用過。
當在不同的線程上調用GetEnumerator方法或者是狀態不是-2(證明已經不是初始狀態了),則GetEnumerator方法會返回一個<TestIterator>d__0類的新實例用來保存不同的狀態。
回到目錄0x03 狀態管理
OK,我們深入了迭代器的內部,發現了原來它的實現主要依靠的是一個狀態機。那么,下面就讓匹夫繼續和大伙聊聊這個狀態機是如何管理狀態的。
狀態切換
根據Ecma-334標準,也就是c#語言標準的第26.2 Enumerator objects小節,我們可以知道迭代器有4種可能狀態:
而其中before狀態是作為初始狀態出現的。
在我們討論狀態如何切換之前,匹夫還要帶領大家回想一下上面提到的,也就是在調用一個使用了迭代器塊,返回類型為一個IEnumerator或IEnumerable接口的方法時,這個方法并非立刻執行我們自己寫的代碼的。而是會創建一個編譯器生成的類的實例,之后當調用MoveNext()方法時(當然如果方法的返回類型是IEnumerable,則要先調用GetEnumerator()方法),我們的代碼才會開始執行,直到遇到第一個yield return語句或yield break語句,此時會返回一個布爾值來判斷迭代是否結束。當下次再調用MoveNext()方法時,我們的方法會繼續從上一個yield return語句處開始執行。
為了能夠直觀的觀察狀態的切換,下面小匹夫提供一個類似于《深入理解C#》這本書中的例子:
class Test {static IEnumerable<int> TestStateChange(){Console.WriteLine("----我TestStateChange是第一行代碼");Console.WriteLine("----我是第一個yield return前的代碼");yield return 1;Console.WriteLine("----我是第一個yield return后的代碼");Console.WriteLine("----我是第二個yield return前的代碼");yield return 2;Console.WriteLine("----我是第二個yield return前的代碼");}static void Main(){Console.WriteLine("調用TestStateChange");IEnumerable<int> iteratorable = TestStateChange();Console.WriteLine("調用GetEnumerator");IEnumerator<int> iterator = iteratorable.GetEnumerator();Console.WriteLine("調用MoveNext()");bool hasNext = iterator.MoveNext();Console.WriteLine("是否有數據={0}; Current={1}", hasNext, iterator.Current);Console.WriteLine("第二次調用MoveNext");hasNext = iterator.MoveNext();Console.WriteLine("是否還有數據={0}; Current={1}", hasNext, iterator.Current);Console.WriteLine("第三次調用MoveNext");hasNext = iterator.MoveNext();Console.WriteLine("是否還有數據={0}", hasNext);} }之后,我們運行這段代碼看看結果如何。
可見,代碼的執行順序就是匹夫剛剛總結的那樣。那么我們將這段編譯后的代碼再反編譯回C#,看看編譯器到底是如何處理這里的狀態切換的。
這里我們只關心兩個方法,首先是GetEnumerator方法。其次是MoveNext方法。
[DebuggerHidden] IEnumerator<int> IEnumerable<int>.GetEnumerator() {if ((Environment.CurrentManagedThreadId == this.<>l__initialThreadId) && (this.<>1__state == -2)){this.<>1__state = 0;return this;}return new Test.<TestStateChange>d__0(0); }看GetEnumerator方法,我們可以發現:
我們再來看看MoveNext方法。
private bool MoveNext() {switch (this.<>1__state){case 0:this.<>1__state = -1;Console.WriteLine("----我TestStateChange是第一行代碼");Console.WriteLine("----我是第一個yield return前的代碼");this.<>2__current = 1;this.<>1__state = 1;return true;case 1:this.<>1__state = -1;Console.WriteLine("----我是第一個yield return后的代碼");Console.WriteLine("----我是第二個yield return前的代碼");this.<>2__current = 2;this.<>1__state = 2;return true;case 2:this.<>1__state = -1;Console.WriteLine("----我是第二個yield return前的代碼");break;}return false; }由于第一次調用MoveNext方法發生在調用GetEnumerator方法之后,所以此時狀態已經變成了0。
可以清晰的看到此時從0——>1——>2——>-1這樣的狀態切換過程。而且還要注意,每個分支中,this.<>1__state都會首先被置為-1:this.<>1__state = -1。之后才會根據不同的階段賦值不同的值。而這些不同的值也就用來標識代碼從哪里恢復執行。
我們再拿之前實現了按順序返回0-9這10個數字的小程序的狀態管理作為例子,來讓我們更加深刻的理解迭代器除了剛剛的例子,還有什么手段可以用來實現“當下次再調用MoveNext()方法時,我們的方法會繼續從上一個yield return語句處開始執行。”這一個功能的。
private bool MoveNext(){switch (this.<>1__state){case 0:this.<>1__state = -1;this.<i>5__1 = 0;while (this.<i>5__1 < 10){this.<>2__current = this.<i>5__1;this.<>1__state = 1;return true;Label_0046:this.<>1__state = -1;this.<i>5__1++;}break;case 1:goto Label_0046;}return false;}如代碼中黃色色帶標出的語句,不錯,此時狀態機是靠著goto語句實現半路插入,進而實現了從yield return處繼續執行的功能。
好吧,讓我們總結一下關于迭代器內部狀態機的狀態切換:
- -2狀態:只有IEnumerable才有,表明在第一次調用GetEnumerator之前的狀態。
- -1狀態:即上文中提到的C#語言標準中規定的Running狀態,表明此時迭代器正在執行。當然,也會用于After狀態,例如上例中的case 2中,this.<>1__state被賦值為-1,但是此時迭代結束了。
- 0狀態:即上文中提到的Before狀態,表明MoveNext()還一次都沒有調用過。
- 正數(1,2,3...),主要用來標識從遇到yield之后,代碼從哪里恢復執行。
0x04 總結
通過匹夫上文的分析,可以看出迭代器的實現的確十分復雜。不過值得慶幸的是很多工作都由編譯器在幕后為我們做好了。那么,本文就到此結束。歡迎大家探討。
本作品采用知識共享署名-非商業性使用-相同方式共享 2.5 中國大陸許可協議進行許可,我的博客歡迎復制共享,但在同時,希望保留我的署名權陳嘉棟(慕容小匹夫),并且,不得用于商業用途。如您有任何疑問或者授權方面的協商,請給我留言。
來源:?<http://www.cnblogs.com/murongxiaopifu/p/4437432.html>?
來自為知筆記(Wiz)
轉載于:https://www.cnblogs.com/zhiGamer/p/6230542.html
總結
以上是生活随笔為你收集整理的匹夫细说C#:庖丁解牛迭代器,那些藏在幕后的秘密的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: DVA框架统一处理所有页面的loadin
- 下一篇: C# 静态类和非静态类(实例类)