C#集合-列舉(Enumeration)

在計算機這個範疇內存在許多種類的集合,從簡單的數據結構好比數組、鏈表,到複雜的數據結構好比紅黑樹,哈希表。儘管這些數據結構的內部實現和外部特徵截然不同,可是遍歷集合的內容確是一個共同的需求。.NET Framework經過IEnumerable和IEnumerator接口實現遍歷集合功能。數據庫

Non-Generic Generic 備註
IEnumerator IEnumerator<T>  
IEnumerable IEnumerable<T> 僅可遍歷
ICollection ICollection<T> 遍歷,可統計集合元素
IDictionary
IList
IDictionary<TKey,TValue>
IList<T>
擁有更過的功能

 

IEnumerable與IEnumerator

IEnumerator接口定義了遍歷協議--在這個協議中,集合中的元素使用向前的方式進行遍歷。它的聲明以下:數組

public interface IEnumerator
{   
    bool MoveNext();
   
    Object Current { get; }

    void Reset();
}

MoveNext將當前元素或指針移動到下一個位置,若是下一個位置沒有元素那麼返回false。Current返回在當前值位置的元素。在獲取集合的第一個元素以前,必須調用MoveNext方法--這對於空集合一樣適用。Reset方法,這移動到初始位置,從而容許集合能夠再次遍歷。Reset更過可能是爲COM互操做而設計:應該儘可能直接避免調用此方法,由於它並無獲得廣泛的支持(直接調用此方法是沒必要要的,由於建立一個新的列舉實例更容易)。安全

集合通常都不實現列舉器,相反,它們經過IEnurable接口提供列舉器數據結構

public interface IEnumerable
{   
    IEnumerator GetEnumerator();
}

經過定義一個單一返回列舉器的方法,IEnumerable接口提供了更多的靈活性,從而各個實現類的遍歷集合的邏輯能夠各部相同。這也就意味着每一個集合的使用者均可以建立本身的方法遍歷集合而不會相互影響。IEnumerable能夠被視做IEnumeratorProvider,它是全部集合類都必須實現的一個接口。ide

下面的代碼演示瞭如何使用IEnumerable和IEnumerator:this

string s = "Hello";

// IEnumerator
IEnumerator rator = s.GetEnumerator();
while (rator.MoveNext())
    Console.Write(rator.Current + ".");

Console.WriteLine();

// IEnumerable
foreach (char c in s)
    Console.Write(c + ".");

通常地,不多調用GetEnumerator方法獲得IEnumerator接口,這是因爲C#提供了foreach語法(foreach語法編譯後,會自動調用GetEnumerator從而遍歷集合),這使得代碼變得更簡潔。spa

 

IEnumerable<T>與IEnumerator<T>

IEnumerator和IEnumerable對應的Generic接口定義以下:設計

public interface IEnumerator<out T> : IDisposable, IEnumerator
{    
       new T Current {
        get; 
    }
}
public interface IEnumerable<out T> : IEnumerable { new IEnumerator<T> GetEnumerator(); }

Generic的Current和GetEnumerator,增長了接口IEnumerable<T>與IEnumerator<T>的類型安全性,避免了對值類型進行裝箱操做,對於集合的使用者更加便利。請注意,數字類型默認實現了IEnumerable<T>接口。3d

正是因爲實現了類型安全的接口,方法Test2(arr)在編譯時就會報錯:指針

static void Main(string[] args)
{
    char[] arr = new char[] { '1', '2', '3' };
    Test1(arr);  // ok
    Test2(arr); // complie-error: cannot convert from char[] to IEnumerable[]

    Console.ReadLine();
}

static void Test1(IEnumerable numbers)
{
    foreach (object i in numbers)
        Console.Write(i + ",");
}

static void Test2(IEnumerable<int> numbers)
{
    foreach (object i in numbers)
        Console.Write(i + ",");
}

請注意,Array默認實現了IEnumerable<T>接口,那麼它同時必然實現了IEnumerable接口。雖然char[]不能轉換成IEnumrable<int>,可是卻能夠轉換成IEnumeable,因此Test1能夠經過編譯,而Test2不能經過編譯(類型轉化失敗錯誤)

對於集合類,對外暴露IEnumerable<T>是標準作法;並須要顯示地實現IEnumerable接口,從而隱藏非Generic的IEnumerable。此時,你再調用GetEnumerator,將獲得IEnumerator<T>。但有時候,爲了兼容非Generic的集合,咱們能夠不遵照這個規則。最好的例子就是數組集合,數組必須返回非generic的IEnumerator以免與早期的代碼衝突。在這種狀況下,爲了獲取IEnumerator<T>,就必須先把數組顯示地轉化爲Generic接口,而後再獲取:

char[] arr = new char[] { '1', '2', '3' };
var rator = ((IEnumerable<char>)arr).GetEnumerator();

幸運的是,你不多須要編寫這樣的代碼,這就要歸功於foreach語句。

 

IEnumerable<T>和IDisposable

IEnumerator<T>繼承了IDisposable。這就容許列舉器能夠擁有資源的引用好比數據庫鏈接,從而確保在遍歷完成後釋放這些資源。foreach會語句會識別這個特性,好比,下面的foreach語句

IList<char> chars =new List<char>(){'a', 'b', 'c'};
 foreach (char c in chars)
     Console.Write(c);

編譯後的代碼爲:

.method private hidebysig static void  Main(string[] args) cil managed
{
 ......
  IL_0026:  callvirt   instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<char>::GetEnumerator()
  IL_002b:  stloc.3
  .try
  {
   .......
System.Collections.Generic.IEnumerator`1<char>::get_Current()
   ......
    IL_0036:  call       void [mscorlib]System.Console::Write(char)
    ......
    IL_003d:  callvirt   instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
  ......
  }  // end .try
  finally
  {
    ......
    IL_0055:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
   ......
  }  // end handler
  ......
} // end of method Program::Main

所以,若是實現了IEnumable<T>接口,執行foreach時,會轉化成調用GetEnumerator<T>, 在遍歷完成以後,釋放IEnumerator<T>。

 

實現列舉接口

當知足下面的一個或多個條件時,須要實現IEnumerable或IEnumerable<T>

  1. 爲了支持foreach語句
  2. 爲了實現除了標準集合以外的集合都是可互操做的
  3. 爲了知足一個複雜集合接口
  4. 爲了支持集合初始化

而實現IEnumerable/IEnumerable<T>,你必須提供一個列舉器,你能夠經過下面三種方式實現

  • 若是類包含了另外集合,那麼須要返回所包含集合的列舉器
  • 在迭遍歷內部使用yield return
  • 實例化IEnumerator/IEnumerator<T>的實現

1)實例IEnumerator/IEnumerator<T>

返回另一個集合的列舉器就是調用內部集合的GetEnumerator。可是,這隻發生在簡單的場景中,在這樣的場景中,內部集合中的元素已經知足須要。另一種更爲靈活的方式是經過yield return語句生成一個迭代器。迭代器(iteraotr)是C#語言特性,該特性用於輔助生產集合,一樣地foreach可與用於iterator以遍歷集合。一個迭代器自動處理IEnumerable和IEnumerator的實現。下面是一個簡單的例子

internal class MyCollection : IEnumerable
{
    int[] data ={ 1, 2, 3 };

    public IEnumerator GetEnumerator()
    {
        foreach (int i in data)
            yield return i;
    }
}

請注意,GetEnumerator根本就沒有返回一個列舉器。依賴於解析yield return後的語句,編譯器編寫了一個隱藏的內嵌列舉器類,而後重構  GetEnumerator實現實例化,最後返回該類。迭代不只功能強大並且簡單。

經過IL代碼,咱們能夠看到確實生產了一個內嵌的列舉器類

image 

 

咱們在上面代碼的基礎上,對MyCollecton作些許修改,使其不只僅實現IEnumerable,還實現IEnumerable<T>

internal class MyCollection : IEnumerable<int>
{
    int[] data ={ 1, 2, 3 };        

    public IEnumerator<int> GetEnumerator()
    {
        foreach (int i in data)
            yield return i;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

由於IEnumerable<T>繼承了IEnumerable,所以咱們必須實現generic的GetEnumerator和非generic的GetEnumerator。按照標準的作法,咱們已經實現了Generic的GetEnumerator。所以對於非Generic的GetEnumerator,咱們直接調用Generic的GetEnumerator便可,這是由於IEnumerable<T>繼承了IEnumerbale。

對應的IL代碼以下:(請注意編譯器實現的IEnumerator<Int32>接口,而再也不是IEnumerator<Object>接口

image

2)在使用yield return返回IEnumerable<T>

咱們建立的類MyCollection能夠作爲複雜集合類的基本實現。可是,若是你不須要實現IEnumerable<T>,那麼應能夠經過yield return語句實現一個IEnumerable<T>,而不是編寫MyCollection這樣的類。也就是說你能夠把迭代邏輯遷移到一個返回IEnumerable<T>的方法中,而後讓編譯器來爲你完成剩餘的事情。

class Program
{
    static void Main(string[] args)
    {

        foreach(int i in GetSomeIntegers())
            Console.WriteLine(i);

        Console.ReadLine();
    }

    static IEnumerable<int> GetSomeIntegers()
    {
        int[] data = { 1, 2, 3 }; 
        foreach (int i in data)
            yield return i;
    }       
}

與之對應的IL代碼

image

從IL代碼中,咱們能夠看到,編譯器一樣生產了一個內部的類,該類實現了IEnumerator<Int32>接口。

3)若是類包含了另外集合,那麼須要返回所包含集合的列舉器

最後一種實現方式將就是編寫一個類直接實現IEnumerator接口。其實這也就是編譯器以前作的事情。在實際中,你不須要這麼作。

首先咱們來實現非Generic的IEnumerator

internal class MyCollection : IEnumerable
{
    int[] data ={ 1, 2, 3 };        

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

    private class Enumerator : IEnumerator
    {
        MyCollection collection;
        int index;

        public Enumerator(MyCollection collection)
        {
            this.collection = collection;
            index = -1;
        }

        public object Current
        {
            get { return collection.data[index]; }
        }

        public bool MoveNext()
        {
            if (index < collection.data.Length-1)
            {
                index++;
                return true;
            }

            return false;
        }

        public void Reset()
        {
            index = -1;
        }
    }
}

而後,咱們在上述代碼的基礎上,實現Generic的IEnumerator

internal class MyCollection : IEnumerable<Int32>
{
    int[] data = { 1, 2, 3 };

    // implement IEnumerable<T>
    public IEnumerator<Int32> GetEnumerator()
    {
        return new Enumerator(this);
    }
    // implement IEnumerable
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    private class Enumerator : IEnumerator<Int32>
    {
        MyCollection collection;
        int index;

        public Enumerator(MyCollection collection)
            {
                this.collection = collection;
                index = -1;
            }

        #region implement IEnumerator<T>
            public int Current
            {
                get { return collection.data[index]; }
            }

            public void Dispose()
            {
            }            

            public bool MoveNext()
            {
                if (index < collection.data.Length - 1)
                {
                    index++;
                    return true;
                }

                return false;
            }

            public void Reset()
            {
                index = -1;
            }
            #endregion

        // implement IEnumerator
        object IEnumerator.Current
        {
            get { return Current; }
        }
    }
}

Generic版本的IEnumerator比非Generic的IEnumberator效率高一些,由於不須要把int轉化成object,從而減小了裝箱的開銷。咱們多看一眼此時對應的IL代碼:

image

顯然地,咱們能夠看到咱們手動建立Enumerator與編譯器生成的Enumerator是同樣的

 

此外,當咱們使用第二種方式的時候,若是咱們有多個IEnumerable<T>的方法,那麼編譯器會產生多個實現了IEnumerator<T>的類

class Program
{
    static void Main(string[] args)
    {

        foreach (int i in GetSomeIntegers())
            Console.WriteLine(i);

        foreach (int i in GetSomeOdds())
            Console.WriteLine(i);

        Console.ReadLine();
    }

    static IEnumerable<Int32> GetSomeIntegers()
    {
        int[] collection = { 1, 2, 3, 4, 5 };
        foreach (int i in collection)
            yield return i;
    }

    static IEnumerable<Int32> GetSomeOdds()
    {
        int[] collection = { 1, 2, 3, 4, 5 };
        foreach (int i in collection)
            if(i%2==1)
                yield return i;
    }       
   
}

對應的IL代碼能夠看到有兩個內部IEnumerator<T>類

image

 

而下面的代碼只會產生一個IEnumerator<T>類

class Program
{
    static void Main(string[] args)
    {

        foreach (int i in GetSomeIntegers())
            Console.WriteLine(i);

        foreach (int i in GetSomeOdds())
            Console.WriteLine(i);

        Console.ReadLine();
    }

    static IEnumerable<Int32> GetSomeIntegers()
    {
        return GetDetails();
    }

    static IEnumerable<Int32> GetSomeOdds()
    {
        return GetDetails(true);
    }

    private static IEnumerable<Int32> GetDetails(bool isOdd = false)
    {
        int[] collection = { 1, 2, 3, 4, 5 };
        int index = 0;

        foreach (int i in collection)
        {
            if (isOdd && i % 2 == 1)
                yield return i;
            if (!isOdd)
                yield return collection[index];
            
            index++;
        }
    }   
}

一樣地,下面的代碼也只會產生一個IEnumerator<T>類

....
static IEnumerable<Int32> GetSomeIntegers()
{
    foreach (int i in GetDetails())
        yield return i;
}

static IEnumerable<Int32> GetSomeOdds()
{
    foreach (int i in GetDetails(true))
        yield return i;
}
....

 

由此,咱們能夠發現,在實現IEnumerable時,特別是有多個實現時,須要注意儘可能減小編譯器生成IEnumerator的類的個數。我猜想在內部,編譯器應該是根據真正不一樣的yield return對於的iterator來肯定IEnumerator類的個數。在個人示例代碼中,產出兩個IEnumerator類時,GetSomeIntegers和GetSomeOdds的yield return的iterator是不一樣的;而在產生一個IEnumerator類時,它們都指向GetDetails的yield return對應的iterator。

 

最後,咱們再來看看IEnumeratorIterator

在網上,並無關於二者的明確區分,或許是我把兩個不應混淆的概念混淆了。下面是我本身的見解,若是不正確,歡迎指正:

1) 實現IEnumerator用於實現IEnumerable,與GetEnumerator方法關聯在一塊兒,從而可使用foreach;並且一旦一個類中肯定了遍歷(MoveNext)的方式以後,那麼就只有這一種方式去遍歷集合了。.NET Framework中大多數集合的IEnumerator都默認向前只讀的方式遍歷集合。

2)Iterator用於遍歷集合,能夠有多個實現方式,惟一的要求是返回IEnumerator<T>,從某種意義上說,Iterator就是IEnumerator。二者的區別是,前者一旦肯定,就只能使用這個方式遍歷集合而後返回一個IEnumerator;然後者能夠在多個方法中以多種方式遍歷集合而後返回不一樣的IEnumerator。(我認爲,二者的差異與IComparable和IComparer的差異相似)。

相關文章
相關標籤/搜索