天天看點

C# —— IEnumerable和狀态機

在上一篇文章中,我們看了一下枚舉器以及.NET如何使用foreach循環,我們看到了枚舉器實際上是如何通過使用MoveNext方法和Current屬性從一個狀态轉換到另一個狀态的對象。

我們知道,如果我們想要建立一個自定義枚舉器,我們将需要實作IEnumerator接口或它的泛型 副本,這是狀态發揮作用和狀态機的地方。檢視枚舉器的Current屬性是如果成為對象還是泛型類型,我們可以利用這個優勢,并實作從計算、到枚舉,甚至整個工作流程的各種算法。所有“魔法”實際上都發生在MoveNext方法中,我們可以在其中執行任何從一個狀态轉換到另一個狀态所需的操作。

所有這些對于了解我們是否希望通過實作IEnumerable接口或者泛型IEnumerable版本來實作我們自己的集合是必不可少的,因為我們必須告訴我們的集合應該如何周遊它,這意味着傳回一個枚舉器并實作MoveNext方法,基本上我們需要實作一個枚舉器,或者如果我們在自己的實作中有一個内部集合,那麼隻需傳遞它。

但在大多數情況下,.NET提供的泛型集合綽綽有餘,除非我們想要建立一個非常專業的疊代器或結構,如圖形和二叉樹。

這就把我們帶到了關于.NET編譯器在幕後做什麼以使我們的生活更輕松的主題,我知道我們已經走了很長一段路,但是要更好地了解它是如何組合在一起的(所做的是值得的)。

現在輸入我們的客人,yield關鍵字。為此,我準備了一個示例,以便更好地可視化yield使用的一個方面

public IEnumerable<int> Fibs (int fibCount)
{
  for (int i = 0, prevFib = 1, curFib = 1; i < fibCount; i++)
  {
    yield return prevFib;
    int newFib = prevFib+curFib;
    prevFib = curFib;
    curFib = newFib;
  }
}
           

我們這裡有一個傳回所有“ fibCount” 斐波那契數列的方法,請注意yield關鍵字。當.NET編譯器遇到yield關鍵字時,它會檢視方法傳回類型(在這種情況下,它是類型  int的IEnumerable),并在背景生成一個枚舉器,是以這實際上會建立一個枚舉整數的對象,是以  yield return組合是相當于枚舉器的Current屬性和方法的其餘部分,直到滿足另一個 yield ,這相當于  MoveNext方法。通過“另一個yield”,我的意思是,就像手動實作的枚舉器一樣,它将保留其目前狀态并在下次遇到 yield時使用它 。是以在這種情況下,第一次時函數将傳回1,第二次調用傳回1,第三次調用傳回2,依此類推,直到  yield超出範圍或方法結束。

你可以在你的方法中擁有任意數量的yield語句,不需要将它放在循環中,并且它将始終從它執行的最後一個傳回行繼續,讓我們看一個例子:

public IEnumerable<int> GetSomeIntegers()
{
  yield return 1;
  yield return 2;
  yield return 3;
}
           

此方法将傳回1,然後在下一次調用時它将傳回2,然後在下一次調用後它将傳回3。

但是yield構造具有另一種形式,也就是yield break,它會告訴枚舉器它已經到達其範圍的末尾,以下是示範:

IEnumerable<string> Foo (bool breakEarly)
{
  yield return "One";
  yield return "Two";

  if (breakEarly)
    yield break;

  yield return "Three";
}
           

此示例僅傳回“One”和“Two”,如果breakEarly參數為true,則永遠不會達到“Three” 。

是以你看,使用yield return和yield break,我們可以設計一個複雜的工作流程,而無需實作我們自己的任何枚舉器,并輕松使用外部參數。

接下來,我将向您展示一個違反正常執行流程的示例,并展示了LINQ如何通過其擴充方法在幕後工作,以及如何編寫枚舉器。

static void Main()
{
  foreach (int fib in EvenNumbersOnly(Fibs(6)))
  {
    Console.WriteLine (fib);
  }
}
    
static IEnumerable<int> Fibs (int fibCount)
{
  for (int i = 0, prevFib = 1, curFib = 1; i < fibCount; i++)
  {
    yield return prevFib;
    int newFib = prevFib+curFib;
    prevFib = curFib;
    curFib = newFib;
  }
}

static IEnumerable<int> EvenNumbersOnly (IEnumerable<int> sequence)
{
  foreach (int x in sequence)
    if ((x % 2) == 0)
      yield return x;
}
           

在這裡,我們有一個枚舉器組合的例子。乍一看,我們希望Fibs首先執行,但這是違反工作流邏輯的部分,程式将首先進入EvenNumberOnly方法,然後當它到達foreach内部時,它才會實際進入Fibs方法。然後它實際上将繼續執行foreach直到它可以傳回一個值,此時它會将它寫入螢幕,然後該過程從它停止的地方再次開始,保持兩個EvenNumberOnly和Fibs枚舉器的狀态直到Fibs完成,此時EvenNumberOnly也将完成。

這就是LINQ如何允許我們連結多個操作并“實時”處理大型資料集,而不是在每個步驟中周遊整個元素集合。使用這種技術,我們還可以處理來自Web服務的分頁資料,而無需預先進行大量調用并将其存儲在記憶體中。

盡管它是一個非常好用且有用的功能,但我們必須記住,該yield 構造有一些限制:

  • 該yield關鍵字隻能用于傳回IEnumerable形式的方法。
  • 該yield關鍵字不能被用在try- catch塊(其理由是,當一個異常被抛出,則枚舉變得無效并且它将被釋放),但它可以被用在try- finally塊。
  • 該方法不能包含ref或out參數。
  • 它不能用于unsafe塊。
  • 它不能用在anonymous方法中,如lambda表達式。

總之,我們看到了如何實作自定義枚舉器,而不必經曆制作我們自己的自定義類型的麻煩,以及我們如何利用它foreach來做更多的事情而不僅僅是疊代一組項目。

原文位址:https://www.codeproject.com/Articles/1266944/IEnumerable-and-State-Machines

繼續閱讀