C#圖解教程 第十八章 枚舉器和迭代器

枚舉器和迭代器

枚舉器和可枚舉類型


第12章中,咱們看到能夠用foreach語句遍歷數組。在本章,咱們會進一步探討數組,來看看爲何它們能夠被foreach語句處理。咱們還會研究如何使用迭代器爲用戶自定義類增長該功能。數組

foreach語句

數組foreach語句爲咱們依次取出數組中的每一個元素。安全

int[] arr1={10,11,12,13};
foreach(int item in arr1)
{
    Console.WriteLine("Item value:  {0}",item);
}

爲何數組能夠這麼作?由於數組能夠按需提供一個叫作枚舉器(enumerator)的對象。枚舉器能夠依次返回請求的數組中的元素。枚舉器「知道」項的次序而且跟蹤它在序列中的位置,而後返回請求的當前項。
對於由枚舉器的類型,必須有一個方法來獲取它。獲取對象枚舉器的方法是調用對象的GetEnumerator方法。實現GetEnumerator方法的類型叫作可枚舉類型(enumerable type或enumerable)。數組是可枚舉類型。
函數

foreach結構設計用來和可枚舉類型一塊兒使用。只要給它的遍歷對象是可枚舉類型,它就會執行以下行爲:編碼

  • 調用GetEnumerator獲取對象枚舉器
  • 從枚舉器中請求每一項而且把它做爲迭代變量(iteration variable),代碼能夠讀取該變量但不能改變

IEnumerator接口


實現了IEnumerator接口的枚舉器包含3個函數成員:Current、MoveNext、Reset。spa

  • Current是返回序列中當前位置項的屬性
    • 它是隻讀屬性
    • 它返回object類型的引用,因此能夠返回任何類型
  • MoveNext是把枚舉器爲知前進到集合中下一項的方法。它返回布爾值,指示新的位置是有效位置仍是已超過序列尾部
    • 若是新位置有效,返回true
    • 若是新位置無效,返回false
    • 枚舉器的原始位置在序列第一項以前,依次MoveNext必須在第一次使用Current前調用
  • Reset是把位置重置爲原始狀態的方法

枚舉器與序列中的當前項保持聯繫的方式徹底取決於實現。能夠經過對象引用、索引值或其餘方式來實現。對於內置的一維數組來講,就是用項的索引。
設計

有了集合的枚舉器,咱們就可使用MoveNext和Current成員來模仿foreach循環遍歷集合中的項。
例:手動作foreach語句自動作的事情。3d

class Program
{
    static void Main()
    {
        int[] MyArray={10,11,12,13};
        IEnumerator ie=MyArray.GetEnumerator();
        while(ie.MoveNext())
        {
            int i=(int)ie.Current;
            Console.WriteLine("{0}",i);
        }
    }
}

IEnumerator接口
可枚舉類是指實現了IEnumerator接口的類。IEnumerator接口只有一個成員–GetEnumerator方法,它返回對象的枚舉器。code

使用IEnumerable和IEnumerator的示例

下面是個可枚舉類的完整示例,類名Spectrum,枚舉器類爲ColorEnumerator。對象

using System;
using System.Collections;
class ColorEnumerator:IEnumerator
{
    string[] _colors;
    int _position=-1;
    public ColorEnumerator(string[] theColors)
    {
        _colors=new string[theColors.Length];
        for(int i=0;i<theColors.Length;i++)
        {
            _colors[i]=theColors[i];
        }
    }
    public object Current
    {
        get
        {
            if(_position==-1||_position>=_colors.Length)
            {
                throw new InvalidOperationException();
            }
            return _colors[_position];
        }
    }
    public bool MoveNext()
    {
        if(_position<_colors.Length-1)
        {
            _position++;
            return true;
        }
        else
        {
            return false;
        }
    }
    public void Reset()
    {
        _position=-1;
    }
}
class Spectrum:IEnumerable
{
    string[] Colors={"violet","blue","cyan","green","yellow","orange","red"};
    public IEnumerator GetEnumerator()
    {
        return new ColorEnumerator(Colors);
    }
}
class Program
{
    static void Main()
    {
        var spectrum=new Spectrum();
        foreach(string color in spectrum)
        {
            Console.WriteLine(color);
        }
    }
}

泛型枚舉接口


目前咱們描述的枚舉接口都是非泛型版本。實際上,在大多數狀況下你應該使用泛型版本IEnumerable<T>IEnumerator<T>。它們叫作泛型是由於使用了C#泛型(參見第17章),其使用方法和非泛型形式差很少。
二者間的本質差異以下:blog

  • 對於非泛型接口形式:
    • IEnumerable接口的GetEnumerator方法返回實現IEnumerator枚舉器類的實例
    • 實現IEnumerator的類實現了Current屬性,它返回object的引用,而後咱們必須把它轉化爲實際類型的對象
  • 對於泛型接口形式:
    • IEnumerable<T>接口的GetEnumerator方法返回實現IEnumator<T>的枚舉器類的實例
    • 實現IEnumerator<T>的類實現了Current屬性,它返回實際類型的對象,而不是object基類的引用

須要重點注意的是,咱們目前所看到的非泛型接口的實現不是類型安全的。它們返回object類型的引用,而後必須轉化爲實際類型。
泛型接口的枚舉器是類型安全的,它返回實際類型的引用。若是要建立本身的可枚舉類,應該實現這些泛型接口。非泛型版本可用於C#2.0之前沒有泛型的遺留代碼。
儘管泛型版本和非泛型版本同樣簡單易用,但其結構略顯複雜。

迭代器


可枚舉類和枚舉器在.NET集合類中被普遍使用,因此熟悉它們如何工做很重要。不過,雖然咱們已經知道如何建立本身的可枚舉類和枚舉器了,但咱們仍是很高興聽到,C#從2.0版本開始提供了更簡單的建立枚舉器和可枚舉類型的方式。
實際上,編譯器將爲咱們建立它們。這種結構叫作迭代器(iterator)。咱們能夠把手動編碼的可枚舉類型和枚舉器替換爲由迭代器生成的可枚舉類型和枚舉器。

在解釋細節前,咱們先看兩個示例。下面的方法實現了一餓產生和返回枚舉器的迭代器。

  • 迭代器返回一個泛型枚舉器,該枚舉器返回3個string類型的項
  • yield return語句聲明這是枚舉中的下一項
public IEnumerator<string>BlackAndWhite()
{
    yield return "black";
    yield return "gray";
    yield return "white";
}

下面方法聲明瞭另外一個版本,並輸出相同結果:

public IEnumerator<string>BlackAndWhite()
{
    string[] theColors={"black","gray","white"};
    for(int i=0;i<theColors.Length;i++)
    {
        yield return theColors[i];
    }
}

迭代器塊

迭代器塊是有一個或多個yield語句的代碼塊。下面3種類型的代碼塊中的任意一種均可以是迭代器塊:

  • 方法主體
  • 訪問器主體
  • 運算符主體

迭代器塊與其餘代碼塊不一樣。其餘塊包含的語句被當作命令式。即先執行代碼塊中的第一個語句,而後執行後面的語句,最後控制離開塊。
另外一方面,迭代器塊不是須要在同一時間執行的一串命令式命令,而是描述了但願編譯器爲咱們建立的枚舉器類的行爲。迭代器塊中的代碼描述瞭如何枚舉元素。
迭代器塊由兩個特殊語句:

  • yield return語句指定了序列中返回的下一項
  • yield break語句指定在序列中沒有的其餘項

編譯器獲得有關枚舉項的描述後,使用它來構建包含全部須要的方法和屬性實現的枚舉器類。結果類被嵌套包含在迭代器聲明的類中。
以下圖所示,根據迭代器塊的返回類型,你可讓迭代器產生枚舉器或可枚舉類型。

使用迭代器來建立枚舉器

class MyClass
{
    public IEnumerator<string> GetEnumerator()
    {
        return BlackAndWhite();    //返回枚舉器
    }
    public IEnumerator<string> BlackAndWhite()//迭代器
    {
        yield return "black";
        yield return "gray";
        yield return "white";
    }
}
class Program
{
    static void Main()
    {
        var mc=new MyClass();
        foreach(string shade in mc)
        {
            Console.WriteLine(shade);
        }
    }
}

下圖演示了MyClass的代碼及產生的對象。注意編譯器爲咱們自動作了多少工做。

  • 圖左的迭代器代碼演示了它的返回類型是IEnumerator<string>
  • 圖右演示了它有一個嵌套類實現了IEnumerator<string>

使用迭代器來建立可枚舉類型

以前示例建立的類包含兩部分:產生返回枚舉器方法的迭代器以及返回枚舉器的GetEnumerator方法。
本節例子中,咱們用迭代器來建立可枚舉類型,而不是枚舉器。與以前的示例相比,本例有如下不一樣:

  • 本例中,BlackAndWhite迭代器方法返回IEnumerable<string>而不是IEnumerator<string>。所以MyClass首先調用BlackAndWhite方法獲取它的可枚舉類型對象,而後調用對象的GetEnumerator方法來獲取結果,從而實現GetEnumerator方法
  • 在Main的foreach語句中,咱們可使用類的實例,也能夠調用BlackAndWhite方法,由於它返回的是可枚舉類型
class MyClass
{
    public IEnumerator<string> GetEnumerator()
    {
        IEnumerable<string> myEnumerable=BlackAndWhite();
        return myEnumerable.GetEnumerator();
    }
    public IEnumerable<string> BlackAndWhite()//迭代器
    {
        yield return "black";
        yield return "gray";
        yield return "white";
    }
}
class Program
{
    static void Main()
    {
        var mc=new MyClass();
        foreach(string shade in mc)
        {
            Console.Write(shade);
        }
        foreach(string shade in mc.BlackAndWhite)
        {
            Console.Write(shade);
        }
    }
}

下圖演示了在代碼的可枚舉迭代器產生泛型可枚舉類型。

  • 圖左的迭代器代碼演示了它的返回類型是IEnumerable<string>
  • 圖右演示了它有一個嵌套類實現了IEnumerator<string>IEnumerable<string>

常見迭代器模式


前面兩節展現了,咱們能夠建立迭代器來返回可枚舉類型或枚舉器。下圖總結了如何使用普通迭代器模式。

  • 當咱們實現返回枚舉器的迭代器時,必須經過實現GetEnumerator來讓類可枚舉,如圖左
  • 若是咱們在類中實現迭代器返回可枚舉類型,咱們可讓類實現GetEnumerator來讓類自己可被枚舉,或不實現GetEnumerator,讓類不可枚舉
    • 若實現GetEnumerator,讓它調用迭代器方法以獲取自動生成的實現IEnumerable的類實例。而後從IEnumerable對象返回由GetEnumerator建立的枚舉器,如圖右
    • 若經過不實現GetEnumerator使類自己不可枚舉,仍然可使用由迭代器返回的可枚舉類,只須要直接調用迭代器方法,若是右第二個foreach語句

產生多個可枚舉類型


下例中,Spectrum類有兩個可枚舉類型的迭代器。注意儘管它有兩個方法返回可枚舉類型,但類自己不是可枚舉類型,由於它沒有實現GetEnumerator

using System;
using System.Collections.Generic;
class Spectrum
{
    string[] colors={"violet","blue","cyan","green","yellow","orange","red"};
    public IEnumerable<string> UVtoIR()
    {
        for(int i=0;i<colors.Length;i++)
        {
            yield return colors[i];
        }
    }
    public IEnumerable<string> IRtoUV()
    {
        for(int i=colors.Length-1;i>=0;i--)
        {
            yield return colors[i];
        }
    }
}
class Program
{
    static void Main()
    {
        var spectrum=new Spectrum();
        foreach(string color in spectrum.UVtoIR())
        {
            Console.Write(color);
        }
        Console.WriteLine();
        foreach(string color in spectrum.IRtoUV())
        {
            Console.Write(color);
        }
        Console.WriteLine();
    }
}

將迭代器做爲屬性


本例演示兩個內容:第一,使用迭代器來產生具備兩個枚舉器的類;第二,演示迭代器如何實現屬性。

using System;
using System.Collections.Generic;
class Spectrum
{
    bool _listFromUVtoIR;
    string[] colors={"violet","blue","cyan","green","yellow","orange","red"};
    public Spectrum(bool listFromUVtoIR)
    {
        _listFromUVtoIR=listFromUVtoIR;
    }
    public IEnumerator<string> GetEnumerator()
    {
        return _listFromUVtoIR?UVtoIR:IRtoUV;
    }
    public IEnumera<string> UVtoIR
    {
        get
        {
            for(int i=0;i<colors.Length;i++)
            {
                yield return colors[i];
            }
        }
    }
    public IEnumerable<string> IRtoUV
    {
        get
        {
            for(int i=colors.Length-1;i>=0;i--)
            {
                yield return colors[i];
            }
        }
    }
}
class Program
{
    static void Main()
    {
        var startUV=new Spectrum(true);
        var startIR=new Spectrum(false);
        foreach(string color in startUV)
        {
            Console.Write(color);
        }
        Console.WriteLine();
        foreach(string color in startIR)
        {
            Console.Write(color);
        }
        Console.WriteLine();
    }
}

迭代器實質


以下是須要了解的有關迭代器的其餘重要事項。

  • 迭代器須要System.Collections.Generic命名空間
  • 在編譯器生成的枚舉器中,Reset方法沒有實現。而它是接口須要的方法,所以調用時老是拋出System.NetSupportedException異常。

在後臺,由編譯器生成的枚舉器類是包含4個狀態的狀態機。

  • Before 首次調用MoveNext的初始狀態
  • Running 調用MoveNext後進入這個狀態。在這個狀態中,枚舉器檢測並設置下一項的爲知。在遇到yield return、yield break或在迭代器體結束時,退出狀態
  • Suspended 狀態機等待下次調用MoveNext的狀態
  • After 沒有更多項能夠枚舉

若是狀態機在Before或Suspended狀態時調用MoveNext方法,就轉到了Running狀態。在Running狀態中,它檢測集合的下一項並設置爲知。
若是有更多項,狀態機會轉入Suspended狀態,若是沒有更多項,它轉入並保持在After狀態。

相關文章
相關標籤/搜索