詳解C# 迭代器[轉]

來源:互聯網
上載者:User

標籤:

      迭代器模式是設計模式中行為模式(behavioral pattern)的一個例子,他是一種簡化對象間通訊的模式,也是一種非常容易理解和使用的模式。簡單來說,迭代器模式使得你能夠擷取到序列中的所有元素 而不用關心是其類型是array,list,linked list或者是其他什麼序列結構。這一點使得能夠非常高效的構建資料處理通道(data pipeline)--即資料能夠進入處理通道,進行一系列的變換,或者過濾,然後得到結果。事實上,這正是LINQ的核心模式。

在.NET中,迭代器模式被IEnumerator和IEnumerable及其對應的泛型介面所封裝。如果一個類實現了IEnumerable接 口,那麼就能夠被迭代;調用GetEnumerator方法將返回IEnumerator介面的實現,它就是迭代器本身。迭代器類似資料庫中的遊標,他是 資料序列中的一個位置記錄。迭代器只能向前移動,同一資料序列中可以有多個迭代器同時對資料進行操作。

在C#1中已經內建了對迭代器的支援,那就是foreach語句。使得能夠進行比for迴圈語句更直接和簡單的對集合的迭代,編譯器會將 foreach編譯來調用GetEnumerator和MoveNext方法以及Current屬性,如果對象實現了IDisposable介面,在迭代 完成之後會釋放迭代器。但是在C#1中,實現一個迭代器是相對來說有點繁瑣的操作。C#2使得這一工作變得大為簡單,節省了實現迭代器的不少工作。

接下來,我們來看如何?一個迭代器以及C#2對於迭代器實現的簡化,然後再列舉幾個迭代器在現實生活中的例子。

1. C#1:手動實現迭代器的繁瑣

假設我們需要實現一個基於環形緩衝的新的集合類型。我們將實現IEnumerable介面,使得使用者能夠很容易的利用該集合中的所有元素。我們的忽 略其他細節,將注意力僅僅集中在如何?迭代器上。集合將值儲存在數組中,集合能夠設定迭代的起始點,例如,假設集合有5個元素,你能夠將起始點設為2, 那麼迭代輸出為2,3,4,0,最後是1. 為了能夠簡單展示,我們提供了一個設定值和起始點的建構函式。使得我們能夠以下面這種方式遍曆集合:

object[] values = { "a", "b", "c", "d", "e" };IterationSample collection = new IterationSample(values, 3);foreach (object x in collection){    Console.WriteLine(x);}

由於我們將起始點設定為3,所以集合輸出的結果是d,e,a,b及c,現在,我們來看如何? IterationSample 類的迭代器:

class IterationSample : IEnumerable{    Object[] values;    Int32 startingPoint;    public IterationSample(Object[] values, Int32 startingPoint)    {        this.values = values;        this.startingPoint = startingPoint;    }    public IEnumerator GetEnumerator()    {        throw new NotImplementedException();    }}

我們還沒有實現GetEnumerator方法,但是如何寫GetEnumerator部分的邏輯呢,第一就是要將遊標的目前狀態存在某一個地方。一方面 是迭代器模式並不是一次返回所有的資料,而是用戶端一次只請求一個資料。這就意味著我們要記錄客戶當前請求到了集合中的那一個記錄。C#2編譯器對於迭代 器的狀態儲存為我們做了很多工作。 現在來看看,要儲存哪些狀態以及狀態存在哪個地方,設想我們試圖將狀態儲存在IterationSample集合中,使得它實現IEnumerator和 IEnumerable方法。咋一看,看起來可能,畢竟資料在正確的地方,包括起始位置。我們的GetEnumerator方法僅僅返回this。但是這 種方法有一個很重要的問題,如果GetEnumerator方法調用多次,那麼多個獨立的迭代器就會返回。例如,我們可以使用兩個嵌套的foreach語 句,來擷取所有可能的值對。這兩個迭代需要彼此獨立。這意味著我們需要每次調用GetEnumerator時返回的兩個迭代器對象必須保持獨立。我們仍舊 可以直接在IterationSample類中通過相應函數實現。但是我們的類擁有了多個職責,這位背了單一職責原則。因此,我們來建立另外一個類來實現 迭代器本身。我們使用C#中的內部類來實現這一邏輯。代碼如下:

class IterationSampleEnumerator : IEnumerator{    IterationSample parent;//迭代的對象  #1    Int32 position;//當前遊標的位置 #2    internal IterationSampleEnumerator(IterationSample parent)    {        this.parent = parent;        position = -1;// 數組元素下標從0開始,初始時預設當前遊標設定為 -1,即在第一個元素之前, #3    }    public bool MoveNext()    {        if (position != parent.values.Length) //判斷當前位置是否為最後一個,如果不是遊標自增 #4        {            position++;        }        return position < parent.values.Length;    }    public object Current    {        get        {            if (position == -1 || position == parent.values.Length)//第一個之前和最後一個自後的訪問非法 #5            {                throw new InvalidOperationException();            }            Int32 index = position + parent.startingPoint;//考慮自訂開始位置的情況  #6            index = index % parent.values.Length;            return parent.values[index];        }    }    public void Reset()    {        position = -1;//將遊標重設為-1  #7    }}

要實現一個簡單的迭代器需要手動寫這麼多的代碼:需要記錄迭代的原創組合#1,記錄當前遊標位置#2,返回元素時,根據 當前遊標和數組定義的起始位置設定定迭代器在數組中的位置#6。初始化時,將當前位置設定在第一個元素之前#3,當第一次調用迭代器時首先需要調用 MoveNext,然後再調用Current屬性。在遊標自增時對當前位置進行條件判斷#4,使得即使當第一次調用MoveNext時沒有可返回的元素也 不至於出錯#5。重設迭代器時,我們將當前遊標的位置還原到第一個元素之前#7。 除了結合當前遊標位置和自訂的起始位置返回正確的值這點容易出錯外,上面的代碼非常直觀。現在,只需要在IterationSample類的GetEnumerator方法中返回我們當才編寫的迭代類即可:

public IEnumerator GetEnumerator(){    return new IterationSampleEnumerator(this);}

值得注意的是,上面只是一個相對簡單的例子,沒有太多的狀態需要跟蹤,不用檢查集合在迭代的過程中是否發生了變化。為了 實現一個簡單的迭代器,在C#1中我們實現了如此多的代碼。在使用Framework內建的實現了IEnumerable介面的集合時我們使用 foreach很方便,但是當我們書寫自己的集合來實現迭代時需要編寫這麼多的代碼。在C#1中,大概需要40行代碼來實現一個簡單的迭代器,現在看看 C#2對這一過程的改進。

2. C#2:通過yield語句簡化迭代    2.1 引入迭代塊(iterator)和yield return 語句

C#2使得迭代變得更加簡單--減少了很多代碼量也使得代碼更加的優雅。下面的代碼展示了再C#2中實現GetEnumerator方法的完整代碼:

public IEnumerator GetEnumerator(){    for (int index = 0; index < this.values.Length; index++)    {        yield return values[(index + startingPoint) % values.Length];    }}

 

簡單幾行代碼就能夠完全實現IterationSampleIterator類所需要的功能。方法看起來很普通,除了使用了yield return。這條語句告訴編譯器這不是一個普通的方法,而是一個需要執行的迭代塊(yield block),他返回一個IEnumerator對象,你能夠使用迭代塊來執行迭代方法並返回一個IEnumerable需要實現的類型,IEnumerator或者對應的泛型。如果實現的是非泛型版本的介面,迭代塊返的yield typeObject類型,否則返回的是相應的泛型型別。例如,如果方法實現IEnumerable<string>介面,那麼yield返回的類型就是String類型。 在迭代塊中除了yield return外,不允許出現普通的return語句。塊中的所有yield return 語句必須返回和塊的最後傳回型別相容的類型。舉個例子,如果方法定義需要返回IEnumeratble<string>類型的話,不能yield return 1 。 需要強調的一點是,對於迭代塊,雖然我們寫的方法看起來像是在順序執行,實際上我們是讓編譯器來為我們建立了一個狀態機器。這就是在C#1中我們書寫的那部 分代碼---調用者每次調用只需要返回一個值,因此我們需要記住最後一次傳回值時,在集合中位置。 當編譯器遇到迭代塊是,它建立了一個實現了狀態機器的內部類。這個類記住了我們迭代器的準確當前位置以及本地變數,包括參數。這個類有點類似與我們之前手寫 的那段代碼,他將所有需要記錄的狀態儲存為執行個體變數。下面來看看,為了實現一個迭代器,這個狀態機器需要按順序執行的操作:

  • 它需要一些初始的狀態;
  • 當MoveNext被調用時,他需要執行GetEnumerator方法中的代碼來準備下一個待返回的資料;
  • 當調用Current屬性是,需要返回yielded的值;
  • 需要知道什麼時候迭代結束是,MoveNext會返回false。
    2.2 迭代器的執行流程

如下的代碼,展示了迭代器的執行流程,代碼輸出(0,1,2,-1)然後終止。

class Program {  static readonly String Padding = new String(‘ ‘, 30);  static IEnumerable<int32> CreateEnumerable()  {      Console.WriteLine("{0} CreateEnumerable()方法開始", Padding);      for (int i = 0; i &lt; 3; i++)      {          Console.WriteLine("{0}開始 yield {1}", i);          yield return i;          Console.WriteLine("{0}yield 結束", Padding);      }      Console.WriteLine("{0} Yielding最後一個值", Padding);      yield return -1;      Console.WriteLine("{0} CreateEnumerable()方法結束", Padding);  }  static void Main(string[] args)  {      IEnumerable<int32> iterable = CreateEnumerable();      IEnumerator<int32> iterator = iterable.GetEnumerator();      Console.WriteLine("開始迭代");      while (true)      {          Console.WriteLine("調用MoveNext方法……");          Boolean result = iterator.MoveNext();          Console.WriteLine("MoveNext方法返回的{0}", result);          if (!result)          {              break;          }          Console.WriteLine("擷取當前值……");          Console.WriteLine("擷取到的當前值為{0}", iterator.Current);      }      Console.ReadKey();  }}

從輸出結果中可以看出一下幾點:

  • 直到第一次調用MoveNextCreateEnumerable中的方法才被調用。
  • 在調用MoveNext的時候,已經做好了所有操作,返回Current屬性並沒有執行任何代碼。
  • 代碼在yield return之後就停止執行,等待下一次調用MoveNext方法的時候繼續執行。
  • 在方法中可以有多個yield return語句。
  • 在最後一個yield return執行完成後,代碼並沒有終止。調用MoveNext返回false使得方法結束。

第一點尤為重要:這意味著,不能在迭代塊中寫任何在方法調用時需要立即執行的代碼--比如說參數驗證。如果將參數驗證放在迭代塊中,那麼他將不能夠很好的起作用,這是經常會導致的錯誤的地方,而且這種錯誤不容易發現。 下面來看如何停止迭代,以及finally語句塊的特殊執行方式。

    2.3 迭代器的特殊執行流程

      在普通的方法中,return語句通常有兩種作用,一是返回調用者執行的結果。二是終止方法的執行,在終止之前執行finally語句中的方法。在上面的例子中,我們看到了yield return語句只是短暫的退出了方法,在MoveNext再次調用的時候繼續執行。在這裡我們沒有寫finally語句塊。如何真正的退出方法,退出方法時finnally語句塊如何執行,下面來看看一個比較簡單的結構:yield break語句塊。 使用 yield break 結束一個迭代

static IEnumerable<int32> CountWithTimeLimit(DateTime limit){    try    {        for (int i = 1; i &lt;= 100; i++)        {            if (DateTime.Now >= limit)            {                yield break;            }            yield return i;        }    }    finally    {        Console.WriteLine("停止迭代!"); Console.ReadKey();    }}static void Main(string[] args){    DateTime stop = DateTime.Now.AddSeconds(2);    foreach (Int32 i in CountWithTimeLimit(stop))    {        Console.WriteLine("返回 {0}", i);        Thread.Sleep(300);    }}

 

轉載自:http://www.yamatamain.com/article/21/1.html

詳解C# 迭代器[轉]

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.