您的位置:首页 > 编程语言 > C#

C#学习笔记(七)—–集合--IEnumerable和IEnumerator接口

2017-05-24 17:56 316 查看

集合

.NET Framework提供了标准的存储和管理类型的集合。其中包括可变大小的列表、链表和排序或不排序字典以及数组,只有数组属于C#本身。其他都是一些类。可以像使用其他类一样对这些类进行实例化。

Framework中的集合类型可以分成以下三类:

①定义了标准集合协议的接口类型

②随时可用的集合类(Dictionary、list)

③编写应用程序特有集合的基类

本笔记将介绍上述的每一个提到的类型,并在额外的章节中介绍某种如何确定元素数量和顺序的类型。

集合的命名空间包括:

System.Collections 非泛型集合类和接口
System.Collections.Specialized 强类型的非泛型集合类
System.Collections.Generic 泛型的集合类和接口
System.Collections.ObjectModel 自定义集合的代理和基类
System.Collections.Concurrent 线程安全的集合


枚举

在计算中,集合类型有很多,包括简单的数据结构(数组和链表)以及一些复杂的数据结构(红/黑数和散列表)。虽然这些类型的实现功能不同,但他们几乎都需要实现一种能够遍历自身内容的功能:Framework是通过一系列的接口(IEnumerator和IEnumerable及其泛型等价物)来支持这个需求。它们允许不同的集合类型使用一组通用的API。部分集合接口的说明如下所示:



IEnumerator和IEnumerable

IEnumerator接口定义了向前方式遍历或枚举集合内容的基本底层协议:

public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}


MoveNext将当前元素或游标向前移动到下一个位置。如果集合没有更多元素,那么它会返回false。Current返回当前位置的元素(通常需要从object转换为更具体的类型)在取出第一个元素之前,我们必须先MoveNext—即使是一个空的集合也是被允许的。Reset的作用是将当前位置移动到起点,再一次遍历集合。前提是Reset被定义的情况下。Reset存在的意义主要在于COM互操作,通常要避免去直接调用它,因为并非所有的可枚举类型都实现了这个方法。并且也有更好的方法去避免调用它,还不如直接重新实例化一个enumerator来的简单呢。

集合类型通常不直接实现IEnumerator,它们通过实现IEnumerable来提供枚举器(Enumerator)。

public interface IEnumerable
{
IEnumerator GetEnumerator();
}


通过定义一个返回枚举器的方法,IEnumerable实现了一种将迭代逻辑转交由其他类(IEnumerator)实现枚举的灵活性,这意味着可以产生多个IEnumerator来供多个使用者使用而互不干扰。IEnumerable可以看作是“IEnumerator”的提供者,它是集合类型需要实现的最基本的接口。

下面的例子说明了IEnumerable和IEnumerator的用法:

string s = "Hello";
// string实现了IEnumerable接口,所以可以调用GetEnumerator方法
IEnumerator rator = s.GetEnumerator();
while (rator.MoveNext())
{
char c = (char) rator.Current;
Console.Write (c + ".");
}
// Output: H.e.l.l.o.


然而。我们很少直接使用这种GetEnumerator的用法,C#提供了一个语法糖:foreach,下面是使用foreach进行重写的语句:

string s = "Hello"; // string实现了IEnumerable接口
foreach (char c in s)
Console.Write (c + ".");


IEnumerable<T>IEnumerator<T>

IEmumerable和IEnumerator几乎总是和从他们扩展出来的泛型版本同时实现:

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


通过定义一个类型化Current和GetEnumerator,这些接口强化了静态类型的安全性(Current不是返回一个object,而是直接返回T)避免了装箱值类型的额外内存开销。这对于用户来说是极好的。数组已经自动实现了
IEnumerable<T>其中T是数组的元素类型
。由于静态类型的安全性得到了改进,给下面这个Test方法放进去一个string类型的数组会产生编译时错误(这里我觉得作者是想表达数组协变的问题,不然为什么给这么个例子?):

void Test (IEnumerable<int> numbers) { ... }


集合类都是公开实现
IEnumerable<T>
接口和显式实现
IEnumerable
接口。这是一种标准的做法。这样,如果直接调用GetEnumerator,则返回一个类型安全的
IEnumerator<T>
。但是,有些时候,这个规则会被向后兼容性破坏(C#2.0之前不支持泛型)。一个很好的反应这个问题的例子是数组:他们必须返回非泛型的IEnumerator,以避免破坏之前的代码。为了回去一个泛型的IEnumerator,你必须强制转换为接口类型:

int[] data = { 1, 2, 3 };
var rator = ((IEnumerable <int>)data).GetEnumerator();


幸好,我们使用foreach,所以很少编写上面那种代码。

IEnumerable<T>和IDisposable

IEnumerable<T>
如果实现了IDisposable的话,它允许枚举器能够保存资源引用(如数据库连接),并在枚举结束或者中途退出后能够正确释放资源。foreach能够识别这个细节,并将下面的语句:

foreach (var element in somethingEnumerable) { ... }


转换为逻辑上的下面的等式:

using (var rator = somethingEnumerable.GetEnumerator())
while (rator.MoveNext())
{
var element = rator.Current;
...
}


uisng语句保证清理操作的执行,后面的章节将继续详细介绍IDisposable。

何时使用非泛型的IEnumerable 接口

由于泛型的IEnumerable具有更高级别的类型安全,所以,存在这样一个问题:还有必要使用非泛型的IEnumerable接口吗(或者ICollection或IList)?

如果你实现了一个IEnumerable的泛型接口,那么你必须同时实现IEnumerable的非泛型,因为前者继承自后者。然而,很少需要从零开始实现这些原始的接口,几乎在所有的情况中,都可以使用迭代器方法、
Collection<T>
接口或者LINQ等高级实现途径来实现。

所以,作为用户该如何选择?几乎在所有的情况中,都可以完全管理泛型接口。但是,有时非泛型接口还是很有作用的。因为它能够在集合众实现所有元素类型的类型统一性。例如,下面的方法能够递归的计算所有集合的元素个数:

public static int Count (IEnumerable e)
{
int count = 0;
foreach (object element in e)
{
var subCollection = element as IEnumerable;
if (subCollection != null)
count += Count (subCollection);
else
count++;
}
return count;
}


需要注意的是这个方法由于是递归的但是方法内没有判断推出的条件而导致方法进入一个死循环。

因为C#给泛型接口提供了协变,所以,似乎给方法提供一个
IEnumerable<object>
参数进去也是有效的(因为泛型的IEnumerable接口继承了非泛型的),然而,遇到值类型的就可能会出错。而在使用未实现
IEnumerable<T>
的传统集合时也会出错,例如windows forms的ControlCollection(不理解。。。希望童鞋们可以解释一下)。

实现Enumeration接口

你可能因为下面的一个或多个原因希望实现IEnumerable或
IEnumerable<T>
:

为了支持foreach语句

为了与任何实现标准组件的集合进行交互

作为一个更复杂集合接口实现的一部分

为了支持集合初始化器

为了实现
IEnumerable或者IEnumerable<T>
,你必须通过如下三个方法来实现:

如果这个类包装了任何一个集合,就返回包装的这个集合的枚举器

使用yield return的迭代器

实例化
IEnumerator或IEnumerator<T>


提示:还可以创建一个现有集合类的子类。
ICollection<T>
正是基于这个目的设计的。另一种方法是使用LINQ查询操作数。

返回另一个collection的枚举器就是调用内部类的GetEnumerator,然而,这也只能针对一些简单的情形,那就是内部类正好是所需要的类型。更好的方法是使用C#的yield return编写迭代器。迭代器是C#的语言特性,它能够协助完成集合的编写,与foreach能够协助完成集合遍历是一样的。迭代器会自动处理IEnumerable和IEnumerator以及他们的泛型等价物。

下面的例子:

public class MyCollection : IEnumerable
{
int[] data = { 1, 2, 3 };
public IEnumerator GetEnumerator()
{
foreach (int i in data)
yield return i;
}
}


注意这个黑魔法:GetEnumerator似乎是不返回迭代器(enumerator)的(对于这句话理解是上面的方法似乎没有返回一个enumerator,确实,字面意思上面它只是有句yield return i,但是这是什么鬼?后面会进行解释)!通过解析yield return语句,编译器会在后台编写一个隐藏的嵌套的枚举器类,然后重构了GetEnumerator来初始化和返回这个类。迭代器是强大并且简单的,并且是LINQ实现的基础。上面这个类在编译器的作用下会生成如下的东西:



使用这种方法,我们也能实现泛型的
IEnumerable<T>
:

public class MyGenCollection : 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
所以必须同时实现这两个泛型和非泛型的GetEnumerator方法。为了与标准方法保持一致,我们还是显示的实现了非泛型的接口方法。它能够调用泛型的GetEnumerator,因为
IEnumerable<T>实现了IEnumerable


我们刚刚编写的类很适合作为编写更复杂的集合的基础,但是,如果我们只需要编写一个简单的
IEnumerable<T>
的实现,那么yield return可以实现更简单的变化。不需要编写类,但是可以将迭代逻辑转移到一个返回泛型
IEnumerable<T>
的方法,并由编译器完成剩余工作。下面的例子:

public class Test
{
public static IEnumerable <int> GetSomeIntegers()
{
yield return 1;
yield return 2;
yield return 3;
}
}


下面是我们使用的方法:

foreach (int i in Test.GetSomeIntegers())
Console.WriteLine (i);
// Output
1
2
3


最后一种编写GetEnumerator的方法是直接写一个实现
IEnumerable
的类,这与编译器解析yield return时的工作方式是一样的。下面的例子定义了一个集合,它写死了,只返回1,2,3三个正数。

public class MyIntList : IEnumerable
{
int[] data = { 1, 2, 3 };
public IEnumerator GetEnumerator()
{
return new Enumerator (this);
}
class Enumerator : IEnumerator // Define an inner class
{ // for the enumerator.
MyIntList collection;
int currentIndex = −1;
public Enumerator (MyIntList collection)
{
this.collection = collection;
}
public object Current
{
get
{
if (currentIndex == −1)
throw new InvalidOperationException ("Enumeration not started!");
if (currentIndex == collection.data.Length)
throw new InvalidOperationException ("Past end of list!");
return collection.data [currentIndex];
}
}
public bool MoveNext()
{
if (currentIndex >= collection.data.Length - 1) return false;
return ++currentIndex < collection.data.Length;
}
public void Reset() { currentIndex = −1; }
}
}


注意Reset方法不是必须的,你甚至可以直接在这个方法里面抛出一个异常。

注意:第一次调用MoveNext会将列表的当前位置移动到第一个而非第二个。为了与迭代器在功能上保持一致,还得实现
IEnumerable<T>
接口。下面这个实例为了保持简洁省略了边界检查代码(上面的代码有边界的检查):

class MyIntList : IEnumerable<int>
{
int[] data = { 1, 2, 3 };
// 泛型的枚举器同时兼容IEnumerable 和IEnumerable<T>.
// 为了避免命名冲突,我们显式的实现非泛型的。
public IEnumerator<int> GetEnumerator() { return new Enumerator(this); }
IEnumerator IEnumerable.GetEnumerator() { return new Enumerator(this); }
class Enumerator : IEnumerator<int>
{
int currentIndex = −1;
MyIntList collection;
public Enumerator (MyIntList collection)
{
this.collection = collection;
}
public int Current { get { return collection.data [currentIndex]; } }
object IEnumerator.Current { get { return Current; } }
public bool MoveNext()
{
return ++currentIndex < collection.data.Length;
}
public void Reset() { currentIndex = −1; }
//既然我们不需要dispose方法,我们可以去显式的实现它,所以他从公共的接口上面隐藏了
void IDisposable.Dispose() {}


这个使用泛型的例子很快,因为Current属性没有装箱操作。

对于foreach,一个类能否用foreach来进行迭代,主要在于这个类是否实现了GetEnumerator的方法。例如上面那个MyIntList类,如果那后面那个“:IEnumerable”去掉,那么这个类不实现这个接口,它只是实现了由这个接口规定的GetEnumerable方法。但是仍然能够用foreach来迭代MyIntList类!这个在编程中叫做鸭子类型:If it looks like a duck, walks like a duck, and quacks like a duck, it’s a duck。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: