C#集合-列举(Enumeration)

2024-08-22 14:48

本文主要是介绍C#集合-列举(Enumeration),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在计算机这个范畴内存在许多种类的集合,从简单的数据结构比如数组、链表,到复杂的数据结构比如红黑树,哈希表。尽管这些数据结构的内部实现和外部特征大相径庭,但是遍历集合的内容确是一个共同的需求。.NET Framework通过IEnumerable和IEnumerator接口实现遍历集合功能。

Non-GenericGeneric备注
IEnumeratorIEnumerator<T> 
IEnumerableIEnumerable<T>仅可遍历
ICollectionICollection<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,它是所有集合类都必须实现的一个接口。

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

复制代码

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从而遍历集合),这使得代码变得更简洁。

 

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>接口。

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

复制代码

static void Main(string[] args)
{char[] arr = new char[] { '1', '2', '3' };Test1(arr);  // okTest2(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 IEnumeratorobject 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的差别类似)。

这篇关于C#集合-列举(Enumeration)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1096578

相关文章

SpringBoot分段处理List集合多线程批量插入数据方式

《SpringBoot分段处理List集合多线程批量插入数据方式》文章介绍如何处理大数据量List批量插入数据库的优化方案:通过拆分List并分配独立线程处理,结合Spring线程池与异步方法提升效率... 目录项目场景解决方案1.实体类2.Mapper3.spring容器注入线程池bejsan对象4.创建

C#实现千万数据秒级导入的代码

《C#实现千万数据秒级导入的代码》在实际开发中excel导入很常见,现代社会中很容易遇到大数据处理业务,所以本文我就给大家分享一下千万数据秒级导入怎么实现,文中有详细的代码示例供大家参考,需要的朋友可... 目录前言一、数据存储二、处理逻辑优化前代码处理逻辑优化后的代码总结前言在实际开发中excel导入很

C#使用Spire.Doc for .NET实现HTML转Word的高效方案

《C#使用Spire.Docfor.NET实现HTML转Word的高效方案》在Web开发中,HTML内容的生成与处理是高频需求,然而,当用户需要将HTML页面或动态生成的HTML字符串转换为Wor... 目录引言一、html转Word的典型场景与挑战二、用 Spire.Doc 实现 HTML 转 Word1

C#实现一键批量合并PDF文档

《C#实现一键批量合并PDF文档》这篇文章主要为大家详细介绍了如何使用C#实现一键批量合并PDF文档功能,文中的示例代码简洁易懂,感兴趣的小伙伴可以跟随小编一起学习一下... 目录前言效果展示功能实现1、添加文件2、文件分组(书签)3、定义页码范围4、自定义显示5、定义页面尺寸6、PDF批量合并7、其他方法

C#下Newtonsoft.Json的具体使用

《C#下Newtonsoft.Json的具体使用》Newtonsoft.Json是一个非常流行的C#JSON序列化和反序列化库,它可以方便地将C#对象转换为JSON格式,或者将JSON数据解析为C#对... 目录安装 Newtonsoft.json基本用法1. 序列化 C# 对象为 JSON2. 反序列化

C#文件复制异常:"未能找到文件"的解决方案与预防措施

《C#文件复制异常:未能找到文件的解决方案与预防措施》在C#开发中,文件操作是基础中的基础,但有时最基础的File.Copy()方法也会抛出令人困惑的异常,当targetFilePath设置为D:2... 目录一个看似简单的文件操作问题问题重现与错误分析错误代码示例错误信息根本原因分析全面解决方案1. 确保

基于C#实现PDF转图片的详细教程

《基于C#实现PDF转图片的详细教程》在数字化办公场景中,PDF文件的可视化处理需求日益增长,本文将围绕Spire.PDFfor.NET这一工具,详解如何通过C#将PDF转换为JPG、PNG等主流图片... 目录引言一、组件部署二、快速入门:PDF 转图片的核心 C# 代码三、分辨率设置 - 清晰度的决定因

C# LiteDB处理时间序列数据的高性能解决方案

《C#LiteDB处理时间序列数据的高性能解决方案》LiteDB作为.NET生态下的轻量级嵌入式NoSQL数据库,一直是时间序列处理的优选方案,本文将为大家大家简单介绍一下LiteDB处理时间序列数... 目录为什么选择LiteDB处理时间序列数据第一章:LiteDB时间序列数据模型设计1.1 核心设计原则

C#高效实现Word文档内容查找与替换的6种方法

《C#高效实现Word文档内容查找与替换的6种方法》在日常文档处理工作中,尤其是面对大型Word文档时,手动查找、替换文本往往既耗时又容易出错,本文整理了C#查找与替换Word内容的6种方法,大家可以... 目录环境准备方法一:查找文本并替换为新文本方法二:使用正则表达式查找并替换文本方法三:将文本替换为图

C#使用Spire.XLS快速生成多表格Excel文件

《C#使用Spire.XLS快速生成多表格Excel文件》在日常开发中,我们经常需要将业务数据导出为结构清晰的Excel文件,本文将手把手教你使用Spire.XLS这个强大的.NET组件,只需几行C#... 目录一、Spire.XLS核心优势清单1.1 性能碾压:从3秒到0.5秒的质变1.2 批量操作的优雅