在计算机这个范畴内存在许多种类的集合,从简单的数据结构比如数组、链表,到复杂的数据结构比如红黑树,哈希表。尽管这些数据结构的内部实现和外部特征大相径庭,但是遍历集合的内容确是一个共同的需求。.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接口定义了遍历协议--在这个协议中,集合中的元素使用向前的方式进行遍历。它的声明如下:

  1. public interface IEnumerator
  2. {
  3. bool MoveNext();
  4.  
  5. Object Current { get; }
  6.  
  7. void Reset();
  8. }

MoveNext将当前元素或指针移动到下一个位置,如果下一个位置没有元素那么返回false。Current返回在当前值位置的元素。在获取集合的第一个元素之前,必须调用MoveNext方法--这对于空集合同样适用。Reset方法,这移动到初始位置,从而允许集合可以再次遍历。Reset更过多是为COM互操作而设计:应该尽量直接避免调用此方法,因为它并没有得到普遍的支持(直接调用此方法是不必要的,因为创建一个新的列举实例更容易)。

集合一般都不实现列举器,相反,它们通过IEnurable接口提供列举器

  1. public interface IEnumerable
  2. {
  3. IEnumerator GetEnumerator();
  4. }

通过定义一个单一返回列举器的方法,IEnumerable接口提供了更多的灵活性,从而各个实现类的遍历集合的逻辑可以各部相同。这也就意味着每个集合的使用者都可以创建自己的方法遍历集合而不会相互影响。IEnumerable可以被视作IEnumeratorProvider,它是所有集合类都必须实现的一个接口。

下面的代码演示了如何使用IEnumerable和IEnumerator:

  1. string s = "Hello";
  2.  
  3. // IEnumerator
  4. IEnumerator rator = s.GetEnumerator();
  5. while (rator.MoveNext())
  6. Console.Write(rator.Current + ".");
  7.  
  8. Console.WriteLine();
  9.  
  10. // IEnumerable
  11. foreach (char c in s)
  12. Console.Write(c + ".");

一般地,很少调用GetEnumerator方法得到IEnumerator接口,这是由于C#提供了foreach语法(foreach语法编译后,会自动调用GetEnumerator从而遍历集合),这使得代码变得更简洁。

IEnumerable<T>与IEnumerator<T>

IEnumerator和IEnumerable对应的Generic接口定义如下:

  1. public interface IEnumerator<out T> : IDisposable, IEnumerator
  2. {
  3. new T Current {
  4. get;
  5. }
  6. }

  7. public interface IEnumerable<out T> : IEnumerable
  8. {
  9. new IEnumerator<T> GetEnumerator();
  10. }

Generic的Current和GetEnumerator,增加了接口IEnumerable<T>与IEnumerator<T>的类型安全性,避免了对值类型进行装箱操作,对于集合的使用者更加便利。请注意,数字类型默认实现了IEnumerable<T>接口。

正是由于实现了类型安全的接口,方法Test2(arr)在编译时就会报错:

  1. static void Main(string[] args)
  2. {
  3. char[] arr = new char[] { '1', '2', '3' };
  4. Test1(arr); // ok
  5. Test2(arr); // complie-error: cannot convert from char[] to IEnumerable[]
  6.  
  7. Console.ReadLine();
  8. }
  9.  
  10. static void Test1(IEnumerable numbers)
  11. {
  12. foreach (object i in numbers)
  13. Console.Write(i + ",");
  14. }
  15.  
  16. static void Test2(IEnumerable<int> numbers)
  17. {
  18. foreach (object i in numbers)
  19. Console.Write(i + ",");
  20. }

请注意,Array默认实现了IEnumerable<T>接口,那么它同时必然实现了IEnumerable接口。虽然char[]不能转换成IEnumrable<int>,但是却可以转换成IEnumeable,所以Test1可以通过编译,而Test2不能通过编译(类型转化失败错误)

对于集合类,对外暴露IEnumerable<T>是标准做法;并需要显示地实现IEnumerable接口,从而隐藏非Generic的IEnumerable。此时,你再调用GetEnumerator,将得到IEnumerator<T>。但有时候,为了兼容非Generic的集合,我们可以不遵守这个规则。最好的例子就是数组集合,数组必须返回非generic的IEnumerator以避免与早期的代码冲突。在这种情况下,为了获取IEnumerator<T>,就必须先把数组显示地转化为Generic接口,然后再获取:

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

幸运的是,你很少需要编写这样的代码,这就要归功于foreach语句。

IEnumerable<T>和IDisposable

IEnumerator<T>继承了IDisposable。这就允许列举器可以拥有资源的引用比如数据库连接,从而确保在遍历完成后释放这些资源。foreach会语句会识别这个特性,比如,下面的foreach语句

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

编译后的代码为:

  1. .method private hidebysig static void Main(string[] args) cil managed
  2. {
  3. ......
  4. IL_0026: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<char>::GetEnumerator()
  5. IL_002b: stloc.3
  6. .try
  7. {
  8. .......
  9. System.Collections.Generic.IEnumerator`1<char>::get_Current()
  10. ......
  11. IL_0036: call void [mscorlib]System.Console::Write(char)
  12. ......
  13. IL_003d: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
  14. ......
  15. } // end .try
  16. finally
  17. {
  18. ......
  19. IL_0055: callvirt instance void [mscorlib]System.IDisposable::Dispose()
  20. ......
  21. } // end handler
  22. ......
  23. } // 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的实现。下面是一个简单的例子

  1. internal class MyCollection : IEnumerable
  2. {
  3. int[] data ={ 1, 2, 3 };
  4.  
  5. public IEnumerator GetEnumerator()
  6. {
  7. foreach (int i in data)
  8. yield return i;
  9. }
  10. }

请注意,GetEnumerator根本就没有返回一个列举器。依赖于解析yield return后的语句,编译器编写了一个隐藏的内嵌列举器类,然后重构  GetEnumerator实现实例化,最后返回该类。迭代不仅功能强大而且简单。

通过IL代码,我们可以看到确实生产了一个内嵌的列举器类

我们在上面代码的基础上,对MyCollecton做些许修改,使其不仅仅实现IEnumerable,还实现IEnumerable<T>

  1. internal class MyCollection : IEnumerable<int>
  2. {
  3. int[] data ={ 1, 2, 3 };
  4.  
  5. public IEnumerator<int> GetEnumerator()
  6. {
  7. foreach (int i in data)
  8. yield return i;
  9. }
  10.  
  11. IEnumerator IEnumerable.GetEnumerator()
  12. {
  13. return GetEnumerator();
  14. }
  15. }

因为IEnumerable<T>继承了IEnumerable,因此我们必须实现generic的GetEnumerator和非generic的GetEnumerator。按照标准的做法,我们已经实现了Generic的GetEnumerator。因此对于非Generic的GetEnumerator,我们直接调用Generic的GetEnumerator即可,这是因为IEnumerable<T>继承了IEnumerbale。

对应的IL代码如下:(请注意编译器实现的IEnumerator<Int32>接口,而不再是IEnumerator<Object>接口)

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

我们创建的类MyCollection可以做为复杂集合类的基本实现。但是,如果你不需要实现IEnumerable<T>,那么应可以通过yield return语句实现一个IEnumerable<T>,而不是编写MyCollection这样的类。也就是说你可以把迭代逻辑迁移到一个返回IEnumerable<T>的方法中,然后让编译器来为你完成剩余的事情。

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5.  
  6. foreach(int i in GetSomeIntegers())
  7. Console.WriteLine(i);
  8.  
  9. Console.ReadLine();
  10. }
  11.  
  12. static IEnumerable<int> GetSomeIntegers()
  13. {
  14. int[] data = { 1, 2, 3 };
  15. foreach (int i in data)
  16. yield return i;
  17. }
  18. }

与之对应的IL代码

从IL代码中,我们可以看到,编译器同样生产了一个内部的类,该类实现了IEnumerator<Int32>接口。

3)如果类包含了另外集合,那么需要返回所包含集合的列举器

最后一种实现方式将就是编写一个类直接实现IEnumerator接口。其实这也就是编译器之前做的事情。在实际中,你不需要这么做。

首先我们来实现非Generic的IEnumerator

  1. internal class MyCollection : IEnumerable
  2. {
  3. int[] data ={ 1, 2, 3 };
  4.  
  5. public IEnumerator GetEnumerator()
  6. {
  7. return new Enumerator(this);
  8. }
  9.  
  10. private class Enumerator : IEnumerator
  11. {
  12. MyCollection collection;
  13. int index;
  14.  
  15. public Enumerator(MyCollection collection)
  16. {
  17. this.collection = collection;
  18. index = -1;
  19. }
  20.  
  21. public object Current
  22. {
  23. get { return collection.data[index]; }
  24. }
  25.  
  26. public bool MoveNext()
  27. {
  28. if (index < collection.data.Length-1)
  29. {
  30. index++;
  31. return true;
  32. }
  33.  
  34. return false;
  35. }
  36.  
  37. public void Reset()
  38. {
  39. index = -1;
  40. }
  41. }
  42. }

然后,我们在上述代码的基础上,实现Generic的IEnumerator

  1. internal class MyCollection : IEnumerable<Int32>
  2. {
  3. int[] data = { 1, 2, 3 };
  4.  
  5. // implement IEnumerable<T>
  6. public IEnumerator<Int32> GetEnumerator()
  7. {
  8. return new Enumerator(this);
  9. }
  10. // implement IEnumerable
  11. IEnumerator IEnumerable.GetEnumerator()
  12. {
  13. return GetEnumerator();
  14. }
  15.  
  16. private class Enumerator : IEnumerator<Int32>
  17. {
  18. MyCollection collection;
  19. int index;
  20.  
  21. public Enumerator(MyCollection collection)
  22. {
  23. this.collection = collection;
  24. index = -1;
  25. }
  26.  
  27. #region implement IEnumerator<T>
  28. public int Current
  29. {
  30. get { return collection.data[index]; }
  31. }
  32.  
  33. public void Dispose()
  34. {
  35. }
  36.  
  37. public bool MoveNext()
  38. {
  39. if (index < collection.data.Length - 1)
  40. {
  41. index++;
  42. return true;
  43. }
  44.  
  45. return false;
  46. }
  47.  
  48. public void Reset()
  49. {
  50. index = -1;
  51. }
  52. #endregion
  53.  
  54. // implement IEnumerator
  55. object IEnumerator.Current
  56. {
  57. get { return Current; }
  58. }
  59. }
  60. }

Generic版本的IEnumerator比非Generic的IEnumberator效率高一些,因为不需要把int转化成object,从而减少了装箱的开销。我们多看一眼此时对应的IL代码:

显然地,我们可以看到我们手动创建Enumerator与编译器生成的Enumerator是一样的

此外,当我们使用第二种方式的时候,如果我们有多个IEnumerable<T>的方法,那么编译器会产生多个实现了IEnumerator<T>的类

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5.  
  6. foreach (int i in GetSomeIntegers())
  7. Console.WriteLine(i);
  8.  
  9. foreach (int i in GetSomeOdds())
  10. Console.WriteLine(i);
  11.  
  12. Console.ReadLine();
  13. }
  14.  
  15. static IEnumerable<Int32> GetSomeIntegers()
  16. {
  17. int[] collection = { 1, 2, 3, 4, 5 };
  18. foreach (int i in collection)
  19. yield return i;
  20. }
  21.  
  22. static IEnumerable<Int32> GetSomeOdds()
  23. {
  24. int[] collection = { 1, 2, 3, 4, 5 };
  25. foreach (int i in collection)
  26. if(i%2==1)
  27. yield return i;
  28. }
  29.  
  30. }

对应的IL代码可以看到有两个内部IEnumerator<T>类

而下面的代码只会产生一个IEnumerator<T>类

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5.  
  6. foreach (int i in GetSomeIntegers())
  7. Console.WriteLine(i);
  8.  
  9. foreach (int i in GetSomeOdds())
  10. Console.WriteLine(i);
  11.  
  12. Console.ReadLine();
  13. }
  14.  
  15. static IEnumerable<Int32> GetSomeIntegers()
  16. {
  17. return GetDetails();
  18. }
  19.  
  20. static IEnumerable<Int32> GetSomeOdds()
  21. {
  22. return GetDetails(true);
  23. }
  24.  
  25. private static IEnumerable<Int32> GetDetails(bool isOdd = false)
  26. {
  27. int[] collection = { 1, 2, 3, 4, 5 };
  28. int index = 0;
  29.  
  30. foreach (int i in collection)
  31. {
  32. if (isOdd && i % 2 == 1)
  33. yield return i;
  34. if (!isOdd)
  35. yield return collection[index];
  36.  
  37. index++;
  38. }
  39. }
  40. }

同样地,下面的代码也只会产生一个IEnumerator<T>类

  1. ....
  2. static IEnumerable<Int32> GetSomeIntegers()
  3. {
  4. foreach (int i in GetDetails())
  5. yield return i;
  6. }
  7.  
  8. static IEnumerable<Int32> GetSomeOdds()
  9. {
  10. foreach (int i in GetDetails(true))
  11. yield return i;
  12. }
  13. ....

由此,我们可以发现,在实现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的差别类似)。

C#集合-列举(Enumeration)的更多相关文章

  1. Java 获取Enumeration类型的集合

    学习到java的io流中关于序列流SequenceInputStream使用,其中把3个以上的流串联起来操作, 使用的参数是生成运行时类型为 InputStream 对象的 Enumeration 型 ...

  2. c#列举和迭代器

    列举 - Enumeration 迭代器是一个值序列(集合)上的一个只读且只向前移动的游标.迭代器要么实现了IEnumerator接口,要么实现了IEnumerator<T>接口. 从技术 ...

  3. 面试题-Java基础-集合和数组

    1.Java集合类框架的基本接口有哪些? 集合类接口指定了一组叫做元素的对象.集合类接口的每一种具体的实现类都可以选择以它自己的方式对元素进行保存和排序.有的集合类允许重复的键,有些不允许.Java集 ...

  4. Java面试准备之集合框架

    集合框架 Collection:List列表,Set集 Map:Hashtable,HashMap,TreeMap Collection 是单列集合 List 元素是有序的(元素存取是有序).可重复 ...

  5. Java面试题:Java中的集合及其继承关系

    关于集合的体系是每个人都应该烂熟于心的,尤其是对我们经常使用的List,Map的原理更该如此.这里我们看这张图即可: 1.List.Set.Map是否继承自Collection接口? List.Set ...

  6. Java面试专题-集合篇(2)

  7. [010] - JavaSE面试题(十):集合之Map

    第一期:Java面试 - 100题,梳理各大网站优秀面试题.大家可以跟着我一起来刷刷Java理论知识 [010] - JavaSE面试题(十):集合之Map 第1问:HashMap和HashTable ...

  8. Java 集合系列11之 Hashtable详细介绍(源码解析)和使用示例

    概要 前一章,我们学习了HashMap.这一章,我们对Hashtable进行学习.我们先对Hashtable有个整体认识,然后再学习它的源码,最后再通过实例来学会使用Hashtable.第1部分 Ha ...

  9. JavaEE基础(十七)/集合

    1.集合框架(HashSet存储字符串并遍历) A:Set集合概述及特点 通过API查看即可 B:案例演示 HashSet存储字符串并遍历 HashSet<String> hs = new ...

随机推荐

  1. Linux下使用RecordMyDesktop进行屏幕录像

    近期我们在评估给用户提供视频教程的可能性,以此来展示某些用视频才能更好表达的教程.在挖掘这个问题的时候,我们发现极丰富的可用于屏幕录像的工具.这些程序大体上特性的区别有:视频质量,性能,兼容性.这在此 ...

  2. Shiro-多Realm验证

    1.多Realm验证 存在这样一种场景,同一个密码可能在MqSQL中存储,也可能在Oracle中存储,有可能MqSQL中使用的是MD5加密算法,而Oracle使用SHA1加密算法.这就需要有多个Rea ...

  3. [Orchard] 通过指定RouteData设置Pager的链接地址

    Orchard 中的Pager是一个很方便的用来分页的Shape, 但默认情况下,它是使用当前Action的地址作为链接地址,如果分页的数据要是由别的Action提供时,这样的分页链接就不对了,其实它 ...

  4. JqueryEasyUI浅谈---视频教程公布

    http://pan.baidu.com/s/1pJqGXez 前两天我在博客园发了一个关于JqueryEasyUI浅谈本地化应用的博客,我简单的介绍了JqueryEasyUI的应用,今天我录制了了一 ...

  5. missing locales

    原文地址:http://codewut.de/content/missing-locales-under-debian This drives me crazy! Every time I deboo ...

  6. tar-usage

    tar -c: 建立压缩档案-x:解压-t:查看内容-r:向压缩归档文件末尾追加文件-u:更新原压缩包中的文件 这五个是独立的命令,压缩解压都要用到其中一个,可以和别的命令连用但只能用其中一个.下面的 ...

  7. Server Develop (九) Simple Web Server

    Simple Web Server web服务器hello world!-----简单的socket通信实现. HTTP HTTP是Web浏览器与Web服务器之间通信的标准协议,HTTP指明了客户端如 ...

  8. jenkins2 pipeline实例

    比较完整的实例,使用了maven打包,git tag,发通知邮件,等待用户deploy,deploy到nexus. 文章来自:http://www.ciandcd.com文中的代码来自可以从githu ...

  9. cookie入门与学习

    据我对cookie诞生背景的了解,cookie是由网景公司创建的,目的就是将用户的数据储存在客户端上.伴随的HTML5的出现,现在又有另外一个解决数据离线储存的方案,就是HTML5中的Web stor ...

  10. webApp 阅读器项目实践

    这是一个webApp 阅读器的项目,是慕课网的老师讲授的一个实战,先给出项目源码在GitHub的地址:https://github.com/yulifromchina/MobileWebReader. ...