实现foreach遍历的两种方式
使用IEnumerable 与 IEnumerator接口
首先先来了解一下这两个接口的含义,从名字常来看,IEnumerator是枚举器的意思,IEnumerable是可枚举的意思。接着看源码:
IEnumerable
public interface IEnumerable
{
// Interfaces are not serializable
// Returns an IEnumerator for this enumerable Object. The enumerator provides
// a simple way to access all the contents of a collection.
[Pure]
[DispId(-4)]
IEnumerator GetEnumerator();
}
IEnumerator
public interface IEnumerator
{
// Interfaces are not serializable
// Advances the enumerator to the next element of the enumeration and
// returns a boolean indicating whether an element is available. Upon
// creation, an enumerator is conceptually positioned before the first
// element of the enumeration, and the first call to MoveNext
// brings the first element of the enumeration into view.
//
bool MoveNext();
// Returns the current element of the enumeration. The returned value is
// undefined before the first call to MoveNext and following a
// call to MoveNext that returned false. Multiple calls to
// GetCurrent with no intervening calls to MoveNext
// will return the same object.
//
Object Current {
get;
}
// Resets the enumerator to the beginning of the enumeration, starting over.
// The preferred behavior for Reset is to return the exact same enumeration.
// This means if you modify the underlying collection then call Reset, your
// IEnumerator will be invalid, just as it would have been if you had called
// MoveNext or Current.
//
void Reset();
}
IEnumerable中只有一个GetEnumerator函数,返回值是IEnumerator类型,所以实现了IEnumerable接口的类可以通过此方法获取一个IEnumerator枚举器,并通过此枚举器遍历这个类中包含的集合中的元素的功能(比如List,ArrayList,Dictionary等继承了IEnumeratble接口的类)。
实现代码
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SuanFa.IEnumerableInterface
{
class InheritIEnumerable : IEnumerable
{
public int[] array = new int[] { 1, 3, 4 };
static void Main()
{
InheritIEnumerable ii = new InheritIEnumerable();
Console.WriteLine("foreach执行结果:");
foreach (int i in ii)
{
Console.WriteLine("i=" + i);
}
}
/// <summary>
/// 返回IEnumerator类型的对象
/// </summary>
/// <returns></returns>
public IEnumerator GetEnumerator()
{
Console.WriteLine("获取枚举器");
return new InheritIEnumerator(array);
}
}
public class InheritIEnumerator : IEnumerator
{
int[] array;
int pos = -1;
public InheritIEnumerator(int []array)
{
this.array = array;
}
public object Current
{
get
{
Console.WriteLine("获取Current");
if (pos<array.Length)
{
return array[pos];
}
else
{
throw new InvalidOperationException();
}
}
}
public bool MoveNext()
{
if (pos<array.Length-1)
{
Console.WriteLine("MoveNext true");
pos++;
return true;
}
else
{
Console.WriteLine("MoveNext false");
return false;
}
}
public void Reset()
{
pos = -1;
}
}
}
运行结果
分析
通过输出结果,可以发现,foreach在运行时会先调用InheritIEnumerable的GetIEnumerator函数获取一个InheritIEnumerator实例(枚举器实例),之后通过循环调用InheritIEnumerator的MoveNext函数,pos后移,更新Current属性,然后返回Current属性,直到MoveNext返回false。 总结一下:
- GetIEnumerator()负责获取枚举器。
- MoveNext()负责让Current获取下一个值,并判断遍历是否结束。
- Current负责返回当前指向的值。
- Rest()负责重置枚举器的状态(在foreach中没有用到)
这些就是IEnumerable,IEnumerator的基本工作原理。 所以foreach就相当于一下代码:
IEnumerable ieable = new InheritIEnumerable();
IEnumerator ie = ieable.GetEnumerator();
while (ie.MoveNext())
{
Console.WriteLine(ie.Current);
}
使用yield
代码实现
class YieldFunctions
{
public static IEnumerable<int> getNums()
{
yield return 1;
yield return 0;
yield return 3;
yield break;
yield return 5;
}
public static void Main()
{
foreach (int i in getNums())
{
Console.WriteLine(i);
}
}
}
- yield return :返回迭代器的内容
- yield break :终止迭代
- yield只能使用在返回类型必须为 IEnumerable、IEnumerable"T"、IEnumerator 或 IEnumerator"T"的方法、运算符、get访问器中
运行结果
关键问题--使用yield迭代的时候我们虽然没有实现GetEnumerator()方法,也没有实现对应的IEnumerator的MoveNext(),和Current属性,但是我们仍然能正常使用foreach,换句话说也就是yield关键字在编译过程中会发生了什么?
YieldFunctions的IL代码
先看一下YieldFunctions类的IL代码的整体框架,主要有三部分:getNums生成的IL代码,新生成的getNums类的IL代码,Main方法的IL代码。
再然后可以看一下MoveNext代码:
总结
用yield来进行迭代的真实流程就是:
- 运行getNums()函数,获取代码自动生成的类的实例(IL代码中getNums类的实例)。
- 接着调用IL代码中GetEnumberator()函数,将获取的类自己作为迭代器开始迭代。
- 每次运行MoveNext(),state增加1,通过switch语句可以让每次调用MoveNext()的时候执行不同部分的代码。
- MoveNext()返回false,结束。
个人总结
- yield关键字其实是一种语法糖,最终还是通过实现IEnumberable"T"、IEnumberable、IEnumberator"T"和IEnumberator接口实现的迭代功能。
- 综上两种对foreach的实现原理,不管哪种方式,其最终原理还是一样的,只是体现了C#的封装性更好,对程序员更加友好,但为了更好的编程,还是需要了解这些原理。
- 可以说,如何可以使用foreach进行遍历的类,都需要直接或间接地实现IEnumerable接口,并返回IEnumerator枚举器对象进行遍历。