实现IEnumerable 和 IEnumerator, 使类型具备枚举功能的注意事项
背景知识
在现实应用中,对于集合以及枚举这些集合的需求非常普遍, 因此在.NET中集合所依赖的接口被设计为公共的。想要实现对象的枚举就必须继承IEnumerable接口。
public interface IEnumerable
{
public IEnumerator GetEnumerator();
}
它只有一个成员:GetEnumerator方法。该方法返回一个世纪的枚举器(enumerator)对象。那么要定义一个枚举器就要必须实现IEnumrator接口:
public interface IEnumerator
{
public Boolean MoveNext();
public Object Current { get; }
public void Reset();
}
这里我不想把MSDN上的解释在重复一遍,我只是提出一个问题,也就是说如果我们希望一个类型支持枚举特性,那么我们就必须还要额外定义一个辅助类型来实现IEnumerator的所有方法。这个辅助类型通常被定义为一个Nested class声明在主类内部。这样在实现的时候我们发现,在app调用GetEnumerator()方法时,我们需要构造一个辅助类型的实例作为返回值。这时我们有两种做法:
- 把主类型的数据当时的静态快照,以复本的形式在辅助类型初始化时赋值给辅助类型实例(enumerator)
- 把主类型的数据的引用传递给辅助类型,保证app访问的实时更新的数据
这的确提供了一定的灵活性给用户, 在app想要枚举某个实例的数据集合时,也可以有两种方法:
- 使用各接口暴露的方法
- 使用foreach语句:foreach( type-identifier in expression ) { embedded-statement }
对于foreach,我想提两句,要想使用它所必须满足的条件:
- expression必须实现GetEnumerator()方法
- GetEnumerator()放回的实例必须实现MoveNext()和Current两个公共方法。
问题1:为什么需要两个接口,而不把Current, MoveNext和Reset成员都放到IEnumerable接口中?
答案:采用这种间接的方式是为了提高灵活性。你可以利用IEnumerator来决定如何展示你想要暴露的内部数据给客户。当然你可以在一个类型中同时实现这两个接口:class MyArrayData : IEnumerable, IEnumerator { ... };这样可以节省了一些创建辅助类型从而带来的CPU和Memory开销。一般建议在如下情况时,使用这种结合的方法:
- 当数据没有存储在系统已有的集合中,否则可以直接把集合的enumerator返回
- 当你的自定义的Enumerator需要做出了移动游标和返回数据之外的其他操作
问题2:如果集合中的数据是值类型,那么有什么性能上的考虑么?
答案:由于值类型在传入ArrayList这样的集合容器中时,需要box,在传出的时候需要unbox。经过很多验证,这种操作时非常耗时的。因此,我们可以在实现标准的枚举方法的时候,可以在暴露一些自定义的GetEnumerator, MoveNext和Current方法(其实, 标准的接口只是调用自定义的接口而已的一个wrapper方法)。这样既可以在自己的方法中避免了不必要的装箱和拆箱操作(用自定义方法,而不能使用foreach语句),又可以被约定俗成的foreach句法所调用(当然,这种情况下就不能避免box和unbox了)。