集合是.NET FCL(Framework Class Library)的重要组成部分,我们平常撸C#代码时免不了和集合打交道,FCL提供了丰富易用的集合类型,给我们撸码提供了极大的便利。正是因为这种与生俱来的便利性,使得我们对集合既熟悉又陌生。很多同学可能一直还是停留在使用的层面上,那么今天我们一起来深入学习一下C#语言中的各种集合。
首先我们看一下 FCL 给我们提供的集合接口:
FCL提供了泛型和非泛型两大类集合类型。因为非泛型集合装箱和拆箱带来的性能开销问题,和泛型集合相比,已经变得越来越鸡肋。所以我们也侧重于泛型集合的分析,但是两者差别不大。
IEnumerable和IEnumeratorIEnumerable接口是所有集合类型的祖宗接口,其作用相当于Object类型之于其它类型。如果某个类型实现了IEnumerable接口,就意味着它可以被迭代访问,也就可以称之为集合类型(可枚举)。IEnumerable接口定义非常简单,只有一个GetEnumerator()方法用于获取IEnumerator类型的迭代器。
我们可以将迭代器想象成数据库的游标,即序列(集合)中的某个位置,迭代器只能在序列(集合)中向前移动。每调用一次MoveNext(),如果序列(集合)中还有下一个元素,则迭代器移动到下一个元素;Current用于获取序列(集合)中的当前元素;因为迭代器调用一次代码只需要获取一个元素,这意味着我们需要确定访问到了序列(集合)中的哪个位置。Reset()用于重置这种状态,但是基本上不会使用Reset()重置状态。
同一个序列(集合)可能同时存在多个迭代器操作,相当于同时对一个集合进行多个遍历。这种情况下可能会出现迭代彼此交错。那么如何解决呢?
集合类不直接支持 IEnumerator 和
IEnumerator 接口。而是直接支持 IEnumerable接口,其唯一方法是 GetEnumerator,此方法用于返回支持 IEnumerator 的对象。每次调用GetEnumerator()方法时都需要创建一个新的对象,同时迭代器必须保存自身的状态,记录此时已经迭代到哪一个元素。这样迭代器就像是序列中的游标。可以有多个游标,移动其中任何一个都可以枚举集合,与其他迭代器互不影响。
for依赖对 Length 属性和索引运算符 ([]) 的支持。借助 Length 属性,C# 编译器可以使用 for 语句迭代数组中的每个元素。for适用于长度固定且始终支持索引运算符的数组,但并不是所有类型集合的元素数量都是已知的。此外,许多集合类(包括 Stack、Queue 和 Dictionary)都不支持按索引检索元素。因此,需要使用一种更为通用的方法来迭代元素集合。假设可以确定第一个、第二个和最后一个元素,那么就没有必要知道元素数量,也没有必要支持按索引检索元素。foreach在这种背景下应运而生。实际上,foreach内部使用迭代器的MoveNext和Current完成元素的遍历。
List<int> list = new List<int>();
List<int>.Enumerator enumerator = list.GetEnumerator();
try
{
int number;
while (enumerator.MoveNext())
{
number = enumerator.Current;
Console.WriteLine(number);
}
}
finally
{
enumerator.Dispose();
}
我们可以自己实现IEnumerable接口和IEnumerator接口实现自定义集合。
实现自定义可枚举类型:
public class MySet : IEnumerable
{
internal object[] values;
public MySet(object[] values)
{
this.values = values;
}
public IEnumerator GetEnumerator()
{
return new MySetIterator(this);
}
}
手写实现自定义迭代器:
public class MySetIterator : IEnumerator
{
MySet set;
/// <summary>
/// 保存迭代到的位置
/// </summary>
int position;
internal MySetIterator(MySet set)
{
this.set = set;
position = -1;
}