给大家讲讲用于枚举元素集合的两个接口IEnumerator和IEnumerable。IEnumerator用于实现一个迭代器(相当于以前C++的文章中所说的iterator),它具备列举一个数据结构中所有元素所需要的一些方法和属性。而IEnumerable接口则用于返回一个迭代器对象,一个实现了IEnumerable的类型表示这个类型对象是可以枚举的。
下面我们先来看看这两个接口的定义吧!
IEnumerator
迭代器用于枚举一个数据结构中的所有元素。
namespace System.Collections
{
[ComVisible(true)]
[Guid(496B0ABE-CDEE-11d3-88E8-00902754C43A)]
public interface IEnumerable
{
[DispId(-4)]
IEnumerator GetEnumerator();
}
}
从上面的定义我们可以看到,一个Emurator具备了枚举一个数据结构中所有元素的最基本能力:
获取当前元素的的能力:Current属性;
移动元素指针的能力:MoveNext方法;
重置迭代器的能力:Reset方法。
这里的Current属性是Object类型,也就是可以返回所有类型元素,而与之对应的泛型接口是:System.Collections.Generic.IEnumerator<T>,它除了继承了Ienumerator之外,还增加了一个特定类型的Current属性。
IEnumerable
IEnumerable声明一个类型为可枚举的类型,而它的定义很简单,就是返回一个迭代器。
namespace System.Collections
{
[ComVisible(true)]
[Guid(496B0ABE-CDEE-11d3-88E8-00902754C43A)]
我们应该对C#风格的遍历语法应该很熟悉了,也就是foreach语句。说到foreach这个东西,其实在C++中也存在,但是是以函数的形式做在库里面的,而对C#来说,它已经被做到语言中去了。在绝大多数情形下,我们应该尽量使用foreach语句来遍历一个集合对象,而不是自己写一个for循环或者其他的while循环等,理由很简单:效率。而foreach语法需要被枚举的对象类型实现了IEnumerable接口。
与IEnumerable对应的泛型接口是:System.Collections.Generic.IEnumerable<T>。
设计一个集合类
通常,IEnumerator和IEnumerable是一起使用的。假设我们设计一个属于自己的一个数据结构类MyCollection,并且让他可以被枚举,那么整体上应该怎么设计呢?我们看看下面的代码。
class MyCollection:IEnumerable
{
public struct MyEmurator : IEnumerator
{
//此处省略实现代码
}
//此处省略部分实现代码
public IEnumerator GetEnumerator()
{
return new MyEmurator(this);
}
}
这是一个典型的对IEnumerator和IEnumerable的应用方式。几乎所有的System.Collection里面的容器都是都是这样来设计的。将容器类型本身实现IEnumerable,表明容器是可枚举的。而迭代器类型则是一个嵌套类型,通过容器类的接口函数GetEnumerator来返回迭代器的实例。通常一个容器和它的迭代器是紧密相关的,并且一个容器配备一个迭代器已经足以,那么将迭代器定义为嵌套类型,避免了管理的混乱。
实现一个2D List类型
我们这里说的二维List类型,其实就是实现一个以List为元素的List,简而言之,这个List2D就是用来存放List的一个List;但是我们枚举的时候,并不想要枚举List2D里面的list, 而是想直接枚举list里面的元素。
我们这里定义了一个泛型类List2D<T> ,实现了IEnumerable<T>接口。这里为了精简代码,这里没有让List2D实现IList接口,只提供了Add、Clear等几个简单方法。不多说了,还是来看看下面的代码吧!
class List2D<T> : IEnumerable<T>
{
//内嵌迭代器类型
public struct Emurator : IEnumerator<T>
{
//此处代码先不写出
}
private List<List<T>> _lists=new List<List<T>>();//存储列表
public List<T> this[int index]
{
get { return _lists[index]; }
}
public int Count
{
get
{
int count = 0;
foreach (List<T> list in _lists)
{
count += list.Count;
}
return count;
}
}
public void Add(List<T> item)
{
_lists.Add(item);
}
public void Clear()
{
_lists.Clear();
}
#region IEnumerable Members
public IEnumerator GetEnumerator()
{
return ((IEnumerable<T>)this).GetEnumerator();
}
#endregion
#region IEnumerable<T> Members
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return new Emurator(this);
}
#endregion
}
我们再来看看迭代器类型struct Emurator的定义,它是嵌套在List2D之中的,它实现了IEnumerator<T>接口。迭代器的实现详见注释。
class List2D<T> : IEnumerable<T>
{
public struct Emurator : IEnumerator<T>
{
List2D<T> _list2D; //被枚举的2D list
IEnumerator<List<T>> _listsEmuretor; //列表迭代器
IEnumerator<T> _listEmuretor; //元素迭代器
bool _started; //是否开始枚举
T _current; //当前元素
public Emurator(List2D<T> list2D)
{
_list2D=list2D;
_listsEmuretor = list2D._lists.GetEnumerator();
_listEmuretor=default(IEnumerator<T>);
_started = false;
_current = default(T);
}
#region IEnumerator Members
public object Current
{
get { return _current; }
}
public bool MoveNext()
{
if (!_started) //第一次MoveNext, 需要取第一个列表
{
_started = true;
if (!_listsEmuretor.MoveNext())
return false;
_listEmuretor = _listsEmuretor.Current.GetEnumerator(); //获取第一个list的迭代器
}
while(true)
{
if (!_listEmuretor.MoveNext())
{
//当前列表枚举结束,需要移动到一个列表
if (!_listsEmuretor.MoveNext())
return false; //所有列表遍历完毕,返回false
_listEmuretor = _listsEmuretor.Current.GetEnumerator();
}
else //当前列表还有元素,成功
{
_current = _listEmuretor.Current;
return true;
}
}
}
public void Reset()
{
_listsEmuretor.Reset();
_current = default(T);
_started = false;
}
#endregion
#region IEnumerator<T> Members
T IEnumerator<T>.Current
{
get { return _current; }
}
#endregion
public void Dispose()
{
}
}
}
真不容易,写了好些代码才把这个2D List的迭代器实现。我们在Main函数里面写一些测试代码,看看它能否正常运行。
static void Main(string[] args)
{
List2D<string> list2D = new List2D<string>();//2维string列表
List<string> list1 = new List<string>();
list1.Add(list1-1);
list1.Add(list1-2);
list1.Add(list1-3);
list2D.Add(list1); //第一个列表有3个元素
List<string> list2 = new List<string>();
list2D.Add(list2);//第二个列表没有元素
List<string> list3 = new List<string>();
list1.Add(list3-1);
list1.Add(list3-2);
list2D.Add(list3); //第三个列表有2个元素
foreach (string str in list2D)//枚举所有string
{
Console.WriteLine(str);
}
Console.ReadKey();
运行结果如下。
list1-1
list1-2
list1-3
list3-1
list3-2
我们可以看到运行结果完全正确。
Yield Return
从前面我们可能发现,有时候写一个迭代器可能是挺麻烦的一个事情,其实需求却可能是挺简单的,但是我们却要写大量代码来实现一个迭代器。所幸的是,C#给我们提供一个独门利器:Yield Return! 利用yield return,我们不需要写一个迭代器就能够实现GetEnumerator函数了。
Yield return不同于普通函数的Return,它从作用上来说,相当于每调用一次yield return,就返回一个被枚举的元素。Yield Return语法只能用在返回值类型为IEnumerator的函数中。 我们现在来看看,List2D的GetEnumerator函数如何用yield return来实现,从而使我们不用再创建一个自定义的Emurator类型。
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
foreach (List<T> list in _lists)
{
foreach (T item in list)
{
yield return item;
}
}
}
7行!包括4个花括弧仅仅用了7行代码,就实现了一个迭代器的所有功能,而前面我们为了实现一个迭代器,写了数十行的代码,这对编码效率上来说,这是多么大的一个提升啊。当然,并非所有情形下,用yield return都是合适,但是在一般的需求之下,它是最好的选择。
习惯了传统编程语言的人,可能会比较难易理解Yield Return这个东西,而且更会对它是到底是如何实现的充满了疑问。其实Yield Return是C#从语言层面上提供的一种简化代码的语法形式而已,而归根到底,其实是C#的编译器辅助我们完成了一部分编码工作—它为我们自动创建了一个匿名的迭代器类型,并且用那个迭代器实现了我们在使用yield return语义所实现的迭代功能。
如果你对上面的论断产生怀疑的话,我们可以借助VS.net自带的IL查看工具ildasm.exe来查看我们用yield return代码编译后的exe文件。Ildasm.exe (VS2005)通常存放在“C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\”目录下。
从下面Ildasm.exe的截图中我们可以看见List2D类型中有个嵌套类型GetEnumerator>d__0<T>,而这个恰恰就是C#编译器为我们生成的匿名迭代器类型。
在感叹C#编译器的智能的时候,我们有必要对yield return的实现做一个更加深入的了解,那就是查看它自动生成的迭代器的代码究竟是怎么样的,与我们自己写的迭代器相比,究竟有多大差别。使用ildasm可以查看IL代码,不过这是非常痛苦的一件事情,即使你对IL的所有指令都背得滚瓜烂熟,同样也是一间非常痛苦的事情。所以,这里我想向大家推荐一个代码学习利器,.Net的反编译器“.Net Reflector”,它可以将编译好的.Net程序反编译为C#代码,并且有着比较高的还原率,而且这个工具本身还在不断升级当中。你可以从http://www.aisto.com/roeder/dotnet免费下载这个软件。
Yield Return可以在GetEnumerator函数中任意地方使用,而我们的程序结构一般会包括顺序结构、循环和分支选择等。为了弄清编译器对yield return的处理细节,我们先设计一个简单的类,它仅仅使用yield return先返回一个-1,再返回一个100,然后就结束了,这是一个再简单不过的顺序执行结构了,下面是这个类的代码:
class TestYieldReturn:IEnumerable
{
public IEnumerator GetEnumerator()
{
yield return -1;
yield return 100;
}
}
我们将这段程序编译好,然后用.NET Reflactor将其进行反编译,于是我们得到了编译器自动生成的迭代器的代码: