在上一篇文章中,我们讲了IComparable和IComparer接口,而在本篇文章中,我要给大家讲讲用于枚举元素集合的两个接口 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将其进行反编译,于是我们得到了编译器自动生成的迭代器的代码:
[CompilerGenerated]
private sealed class <GetEnumerator>d__0 : IEnumerator<object>, IEnumerator, IDisposable
{
// Fields
private int <>1__state;
private object <>2__current;
public TestYieldReturn <>4__this;
// Methods
[DebuggerHidden]
public <GetEnumerator>d__0(int <>1__state)
{
this.<>1__state = <>1__state;
}
private bool MoveNext()
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
this.<>2__current = -1;//首先返回-1
this.<>1__state = 1;
return true;
case 1:
this.<>1__state = -1;
this.<>2__current = 100;//再返回100
this.<>1__state = 2;
return true;
case 2:
this.<>1__state = -1; //已经到了末尾,设置状态为结束
break;
}
return false;
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
void IDisposable.Dispose()
{
}
// Properties
object IEnumerator<object>.Current
{
[DebuggerHidden]
get
{
return this.<>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return this.<>2__current;
}
}
}
我们能看到这个迭代器类型的前面有个Attribute:[CompilerGenerated],这表明这个迭代器确实属于编译器生成无疑。迭代器的成 员变量除了current和被枚举对象“<>4__this”之外,还增加了一个“<>1__state”的成员,而这个 state,真是用于实现顺序程序结构的状态变量,我们可以分析一下最重要的MoveNext函数,State的0、1、2分别代表第一个yield return之前,第一个yield return之后,第二个yield return之后的程序状态,而-1代表枚举结束。编译器就是用state成员变量来控制顺序逻辑的先后次序。
类似的,C#编译器通过设定一些其他的辅助变量,来实现循环和分支等控制结构。有时候这几种结构是相互组合的,编译器都能够很好的实现他们。
Version控制
我们来看看这段代码,它想把列表中的所有负数给删除掉。
List<int> list=new List<int>();
list.Add(0);
list.Add(-1);
list.Add(2);
foreach(int i in list)
{
if(i<0) list.Remove(i);
}
这段代码看起来似乎写的没有问题,编译也能通过。但是真正跑起来就会抛出异常了。它将会抛出一个InvalidOperationException,错 误信息是"Collection was modified; enumeration operation may not execute."。
怎么会这样呢?原来是微软在定义迭代器功能的时候,要求在枚举开始到结束的这个过程之中,应该要求保证被枚举的对象不能够增加或者减少元素。其实这个需求也很好理解,相信谁也不想在清点队伍人数的时候,队伍里面的人还在出出进进吧?
这里善于思考的读者可能马上会想到一个问题,迭代器又是如何知道这个集合发生改变了呢?这里微软用了一个简单而巧妙的办法,给集合对象设置了一个版本信息,通过在迭代过程中不断检查版本信息,来判断集合对象是否已经被更新。具体的做法如下。
1. 给集合对象增加一个成员private int _version,初始值设置为0;
2. 在所有直接对集合元素进行更新的操作中,例如insert、remove等等,都将_version ++;
3. Enumerator也设置一个_version成员,并且在构造函数中将其设置为集合对象的_version成员。其实也就是记录了collection当时的版本。
4. Enumerator在迭代的过程中,不断检查collection的_version字段,一旦发现与当初记录的_version值不同,则认定为集合已经被更新,立即抛出异常。
一点感言
从foreach到yield return还有C#3.X的LINQ等等,我们发现C#正在为我们越来越多的事情,但是我们不知道这种变化何时是尽头,也不知道我们的学习何时是尽头。 C#引入这些新特性是希望不断简化我们的编码工作,但是与此同时难免也会带来一些新的问题,那就是C#正在变得越来越庞大和复杂,也许有一天我们会发现, 自己都不知道什么是C#了,也许把什么都做入语言层面并非是一件好事。天天有新技术的感觉是不错,不过昨天的新技术在今天立马就变成了旧技术却可能是一件 令人沮丧的事情。我们需要创新,但是更加需要经典,C#何时才能成为经典?