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的差异相似)。

相关文章
相关标签/搜索