实现IEnumerator 已支持foreach

      本文严重参考了 笔记:IEnumerable和IEnumerator(包括泛型版),  另外caozhy在BBS也有一段总结(链接地址), 可以参考


1.  一个对象可以用foreach遍历, 必须包含以下3个:
    一个GetEnumerator()方法, 无参数,返回类型任意
    Current属性,只有get方法,不能有set方法(编译器也不允许),返回类型任意
    MoveNext()方法,无参数, 判断是否到遍历完毕,返回类型bool
    为了让Current属性知道应该返回哪个值了, 和 辅助MoveNext判断是否遍历完毕, 通常还会有个 int index, 虽然不必须但很有必要

2.  实现了上面3个方法和属性, 代码就可以使用foreach 了, 编译器会自动去绑定,因此不需要IEnumerable和IEnumerator。
    其实接口的本质就是让编译器去绑定一个对象的方法,在这里,C#编译器的确不依赖接口
    //手动让一个int数组支持foreach
    class A
    {
        int[] array = new int[]{1,2,3,4,5};
        int index=-1;
        public A GetEnumerator() { return this; }
        public int Current { get { return array[index]; } }
        public bool MoveNext() { index++; return index < array.Length ? true : false; }
    }

    class TaskThread
    {
        static void Main()
        {
            A a = new A();
            foreach (var s in a)
                Console.WriteLine(s);
        }
    }

3.  IEnumerable接口指明需要GetEnumerator()
    IEnumerator接口指明需要Current、MoveNext(), 但VS中点击 "实现接口", 通常还会生成Reset()方法.
    一般我们对一个数组变量连续2次使用 foreach, 就会循环两遍; 如果你仅实现了上面的三个方法和属性, 连续2次foreach却不能达到要求
    class A
    {
        int[] array = new int[]{1,2,3,4,5};
        int index=-1;
        public A GetEnumerator() { return this; }
        public int Current { get { return array[index]; } }
        public bool MoveNext() 
        { 
            index++;
            if (index < array.Length)
                return true;
            else
            {
                //Reset();
                return false;
            }
        }

        //private void Reset()
        //{
        //    index = -1;
        //}
    }

    class TaskThread
    {
        static void Main()
        {
            A a = new A();
            foreach (var s in a)
                Console.WriteLine(s);
            Console.WriteLine("循环第二次");
            foreach (var s in a)
                Console.WriteLine(s);
        }
    }
    原因是: 第一次循环结束后, index就已经等于array.Length了; 第二次循环一开始, foreach会首先进行MoveNext()判断, 判断结果为false, 当然立马退出咯
    当把注释掉的 Reset() 方法恢复后, 就可以得到正常结果了.


4.  IEnumerable和IEnumerator通过IEnumerable的GetEnumerator()方法建立了连接,可以通过IEnumerable的GetEnumerator()得到IEnumerator对象
    那为什么集合类不直接继承(支持)IEnumerator<T>和IEnumerator接口?
    假如同时有两个循环交错遍历同一个集合(一个foreach嵌套了另外一个foreach),那么集合必须维持当前元素的一个状态指示器,确保当调用MoveNext()方法时,能正确定位下一个元素。
    为了解决这个问题,集合类不直接支持IEnumerator<T>和IEnumerator接口。而是通过IEnumerable的GetEnumerator返回一个新的IEnumerator对象来负责维护循环遍历的状态,IEnumerator(迭代器,或者叫枚举数)相当于一个“游标”(cursor)或者“书签”。可以有多个书签,移动每个书签都可独立于其他书签来遍历集合
下面的代码是双线程同时访问集合

    class A
    {
        int[] array = new int[]{1,2,3,4,5};
        int index=-1;
        public A GetEnumerator() { return this; }
        public int Current { get { return array[index]; } }
        public bool MoveNext() 
        { 
            index++;
            if (index < array.Length)
                return true;
            else
            {
                Reset();
                return false;
            }
        }

        private void Reset()
        {
            index = -1;
        }
    }

    class TaskThread
    {
        static A a = new A();
        static void Main()
        {
            Thread m = new Thread(GetSum);
            Thread n = new Thread(GetSum);
            m.Start();
            n.Start();
        }

        static void GetSum()
        {
            int sum = 0;
            foreach (var s in a)
            {
                Thread.Sleep(100);  //模拟大量运算, 否则看不出效果
                sum += s;
            }
            Console.WriteLine("ThreadID={0}, sum={1}",Thread.CurrentThread.ManagedThreadId,sum);
        }

运行的结果是
ThreadID=3, sum=5
ThreadID=4, sum=26
按理正常结果应该是 15, 可以看出两个线程都分别干扰了对方的index


5.  由于IEnumerable<T>扩展(继承)了旧的IEnumerable接口,所以要实现两个不同的方法:
    IEnumerator<T> GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator; // 由于和泛型版本的方法同名,所以该方法的实现需要使用显式接口实现


6.  我们通过ILSpy 看看源码中 List 的IEnumerator定义

public struct Enumerator : IEnumerator<T>, IDisposable, IEnumerator
{
	private List<T> list;
	private int index;
	private int version;
	private T current;
	[__DynamicallyInvokable]
	public T Current
	{
		[__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
		get
		{
			return this.current;
		}
	}
	[__DynamicallyInvokable]
	object IEnumerator.Current
	{
		[__DynamicallyInvokable]
		get
		{
			if (this.index == 0 || this.index == this.list._size + 1)
			{
				ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumOpCantHappen);
			}
			return this.Current;
		}
	}
	internal Enumerator(List<T> list)
	{
		this.list = list;
		this.index = 0;
		this.version = list._version;
		this.current = default(T);
	}
	[__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
	public void Dispose()
	{
	}
	[__DynamicallyInvokable]
	public bool MoveNext()
	{
		List<T> list = this.list;
		if (this.version == list._version && this.index < list._size)
		{
			this.current = list._items[this.index];
			this.index++;
			return true;
		}
		return this.MoveNextRare();
	}
	private bool MoveNextRare()
	{
		if (this.version != this.list._version)
		{
			ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
		}
		this.index = this.list._size + 1;
		this.current = default(T);
		return false;
	}
	[__DynamicallyInvokable]
	void IEnumerator.Reset()
	{
		if (this.version != this.list._version)
		{
			ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
		}
		this.index = 0;
		this.current = default(T);
	}
}




后记: 我现在很疑惑非要实现这两个接口,  for ( int i = 0; i < length; i++) 这种遍历方式的速度还要快些, 如果不是泛型, 比如int[]数组, 即便是用了foreach, 编译器也会优化成for循环, 那实现这两个接口仅仅是为了让别人用 foreach ?




  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值