C#中,如果实现遍历一个数组,除了for循环,还可以是foreach循环。在foreach循环中,我们只需要创建一个同类型的值,来表示我们遍历后的值就可以了。但是实际上,只有实现了IEnumerable接口的类型,才能使用foreach遍历。
那么什么是迭代器呢:
我们先手动实现以下迭代,我们使用迭代器写个和foreach类似的功能来遍历一个字符串,输出它每个字符。在foreach前面调用它:
static void Main()
{
string str = "ABCDEFG";
foreachFunc(str);
foreach (char a in str)
{
Console.WriteLine("官方foreach里的循环是:" + a);
}
}
static void foreachFunc(string str)
{
IEnumerator e = str.GetEnumerator();
while (e.MoveNext())
{
Console.WriteLine("民间foreach里的循环是:" + e.Current);
}
}
实现的效果是一样的:
我们发现民间的foreach也是可以完成工作的。
string 里面有一个GetEnumerator方法,这个方法返回一个IEnumerator的对象。一个官方定义的数据元素的数组,一般都继承了这个IEnumerable和IEnumerator接口,来配合foreach实现遍历的操作。
枚举接口IEnumerable和IEnumerator是迭代器模式(iterator pattern)在C#中的实现。它们实现在集合上进行简单迭代的效果。
1.IEnumerable接口定义了一个可以返回IEnumerator类型对象的方法:GetIEnumerator。
2.IEnumerator接口在它内部的字段和方法主要有三个:
代码中有可能出现两个不同的迭代器对同一个序列进行迭代,我们需要两个状态能被正确的处理,所以C#把枚举接口分为IEnumerator和IEnumerable。而为了不违背单一职责原则,IEnumerable本身没有实现MoveNext方法。
我们可以自定义类来手动实现枚举接口的功能:
我们先定义IEnumerable的接口的类,里面存放一个数组:
class GameEnumerable : IEnumerable
{
private string[] Games = new string[5] { "彩虹六号", "赛博朋克", "骑马与砍杀", "神界原罪", "刺客信条" };
public IEnumerator GetEnumerator()
{
return new GameEnumerator(Games);
}
}
然后我们再定义实现IEnumerator接口的类,同样的,用数组索引的加减来实现迭代:
class GameEnumerator : IEnumerator
{
private string[] Games;
private int position = -1;
//用于遍历的标志索引,一般默认值为-1,以便于第一次输出就能输出0
public GameEnumerator(string[] gamenames)
{
Games = new string[gamenames.Length];
for (int i = 0; i < Games.Length; i++)
{
Games[i] = gamenames[i];
}
}
public object Current
{
//Current只读,且要注意索引position越界的情况
get
{
if(position>=Games.Length)
{
return null;
}
return Games[position];
}
}
bool IEnumerator.MoveNext()
{
if (position < Games.Length)
{
position++;
return true;
}
return false;
}
void IEnumerator.Reset()
{
position = -1;
}
}
然后我们在主函数中创建IEnumerable的实例,然后使用IEnumerator来接受它。
注意,实现IEnumerator接口的类的MoveNext方法使用了接口约束,所以只有IEnumerator接口的对象接受GameEnumerator 类的实例才能访问到MoveNext方法。
然后我们在主函数中调用MoveNext方法就可以实现遍历了:
static void Main()
{
GameEnumerable enumerable = new GameEnumerable();
IEnumerator game = enumerable.GetEnumerator();
while(game.MoveNext())
{
if (game.Current == null)
{
break;
}
Console.WriteLine("当前数组里的游戏是" + game.Current);
}
}
效果和我们想象的一样:
但是很明显,这样来进行遍历太繁琐了,但在C# 1.0里,一切都是这么发生的,如果想要加一点灵活性,可以使用IEnumerable和IEnumerator的泛型版本,我们上面的改一改就变成这样:
class Program
{
static void Main()
{
Console.WriteLine("请输入需要的数组长度:");
int Length = int.Parse(Console.ReadLine());
int[] array = new int[Length];
for (int i = 0; i < Length; i++)
{
Console.WriteLine("请输入第" + (i + 1) + "个数");
array[i] = int.Parse(Console.ReadLine());
}
GameEnumerable enumerable = new GameEnumerable(array);
IEnumerator game = enumerable.GetEnumerator();
while(game.MoveNext())
{
if (game.Current == null)
{
break;
}
Console.WriteLine("当前数组里的是" + game.Current);
}
}
}
class GameEnumerable : IEnumerable
{
private T[] Games;
public GameEnumerable(T[] games)
{
Games = new T[games.Length];
for (int i = 0; i < Games.Length; i++)
{
Games[i] = games[i];
}
}
public IEnumerator GetEnumerator(int i)
{
return null;
}
public IEnumerator GetEnumerator()
{
return new GameEnumerator(Games);
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)Games).GetEnumerator();
}
}
class GameEnumerator : IEnumerator
{
private T[] Games;
private int position = -1;
//用于遍历的标志索引,一般默认值为-1,以便于第一次输出就能输出0
public GameEnumerator(T[] gamenames)
{
Games = new T[gamenames.Length];
for (int i = 0; i < Games.Length; i++)
{
Games[i] = gamenames[i];
}
}
public object Current
{
//Current只读,且要注意索引position越界的情况
get
{
if(position>=Games.Length)
{
return null;
}
return Games[position];
}
}
T IEnumerator.Current
{
get
{
if (position >= Games.Length)
{
return default(T);
}
return Games[position];
}
}
public void Dispose()
{
throw new NotImplementedException();
}
bool IEnumerator.MoveNext()
{
if (position < Games.Length)
{
position++;
return true;
}
return false;
}
void IEnumerator.Reset()
{
position = -1;
}
}
有点长,不过功能还是一样的,区别在于我们可以自定义类型,我们这里尝试了一下int型然后输出:
不过要注意的是:由于IEnumerable
上面的遍历虽然说功能实现了,但是逻辑比较复杂,而且由于定义了接口的原因,里面的字段和方法的要求特别多。C#2.0以后引入了迭代器,简化了上述的流程。
迭代器的声明格式为:
IEnumerable/IEnumerator FunctionName()
{
yield return ...
}
它的返回值是IEnumerable或IEnumerator的对象,后面跟随的是函数的名字,然后可以在一个逻辑分支里多次执行yield return 语句来返回,不同的是,在yield return执行完毕以后,函数并不会销毁,而是“休克”,等待返回值的IEnumerator执行下一次MoveNext();
迭代器返回的IEnumerator对象没有手动实现例如上文中的MoveNext、Current的方法。它使用一个或多个yield return语句告诉编译器创建枚举器类,yield return语句指定了枚举器中下一个可枚举项,迭代器在每次调用MoveNext函数时,会顺着上一次的枚举项(yield return)按照我们自己写的逻辑执行到下一个枚举项去。
在迭代器中需要注意的是:
我们看个例子:
static void Main()
{
Console.WriteLine("请输入您需要的数组的长度");
int Length = int.Parse(Console.ReadLine());
int[] Array = new int[Length];
IEnumerable enumerable = Function(Length);
IEnumerator enumerator = enumerable.GetEnumerator();
int i = 0;
while (enumerator.MoveNext())
{
Array[i] = (int)enumerator.Current;
i++;
}
Console.WriteLine("数组里的值是:");
foreach (int t in Array)
{
Console.Write(t.ToString()+" ");
}
Console.WriteLine(" ");
}
static IEnumerable Function(int Length)
{
for (int i = 0; i < Length; i++)
{
Console.WriteLine("请输入您需要放在数组里的值:");
int x = int.Parse(Console.ReadLine());
Console.WriteLine("此时我们要放入数组的值是:" + x);
yield return x;
}
}
这个例子中,我们迭代器中有一个for循环,每次循环都yield return一次,返回的值放入我们的数组中。每次我们输入一个值,可以通过Current来传递给我们在主函数里声明的数组。当然,我们也可以不用IEnumerable返回,可以直接使用IEnumerator来返回,代码还可以精简一丢丢。
看一下结果:
我们再看一个例子:
在这个例子中,我们实时监测MoveNext的返回值情况,我们设定一个迭代器中循环的值X,当我们迭代器中循环超出5的时候我们将执行yield break,即迭代终止:
static void Main()
{
Console.WriteLine("输入你想从何时迭代结束");
int x = int.Parse(Console.ReadLine());
IEnumerable ienumerable = TestStateChange(x);
IEnumerator ienumerator = ienumerable.GetEnumerator();
Console.WriteLine("主函数:第一次调用MoveNext,迭代器开始运行");
bool Next = ienumerator.MoveNext();
Console.WriteLine("主函数:是否有数据" + Next + ",Current:" + ienumerator.Current);
Console.WriteLine("主函数:第二次调用MoveNext");
Next = ienumerator.MoveNext();
Console.WriteLine("主函数:是否有数据" + Next + ",Current:" + ienumerator.Current);
Console.WriteLine("主函数:第三次调用MoveNext");
Next = ienumerator.MoveNext();
Console.WriteLine("主函数:是否有数据" + Next + ",Current:" + ienumerator.Current);
}
static IEnumerable TestStateChange(int count)
{
Console.WriteLine("迭代器:我是第一行代码");
Console.WriteLine("迭代器:我是第一个YieldReturn前的");
yield return 1;
Console.WriteLine("迭代器:我是第一个YieldReturn后的代码");
for (int i = 0; i < count; i++)
{
Console.WriteLine("迭代器:这是第" + i + "次了");
if (i > 5)
{
yield break;
}
}
Console.WriteLine("迭代器:我是第二个YieldReturn前的代码");
yield return 2;
Console.WriteLine("迭代器:我是第二个YieldReturn后的代码");
}
我们首先先设定只循环3次,小于5,不会迭代终止,输出为:
我们可以观察到:
我们如果输入大于5的值,导致yield break会如何呢。
我们看到:
执行了yield break后,迭代器立即停止,MoveNext立马返回false,且Current保留在最后一次return的值上。
我们可以画一张图来表示IEnumerable和IEnumerator和迭代器的关系:
迭代器在我们看不见的地方,实际上的原理就是一个状态机,迭代器有四种可能状态,分别是Before状态、Running状态、Suspended状态、After状态。这四个状态的的转换是这样的:
由图我们可以看到,是
在编译器的内部,我们的迭代器实际上生成了一个类,该类继承了IEnumerator接口,并且在内部创建了有关于迭代器的Current字段和MoveNext函数,MoveNext函数实际上是一个很大的Switch语句,它实现yield return功能实际上靠着goto语句半路插入才能使迭代器能从yield return语句处执行。
我们平常普通情况下的return关键字的用法一般有两个:
在C#中由于Finally语句比return语句的优先级高,所以Try-Catch语句可以由return语句退出,但是finally里面的逻辑是不会跳过的,但是对于泛型迭代器来说,相较于非泛型的迭代器,泛型迭代器继承了IDisposable接口,由此也多了一个Dispose方法。
在泛型迭代器中,如果存在Finally语句,如果不使用Foreach语句或者显式调用Dispose方法,将不会执行finally逻辑块。
由于foreach会在它自身的finally语句中调用IEnumerator所提供的Dispose方法。在迭代器迭代完成之前,若yield return 语句处于try-catch逻辑块中,如果调用了泛型迭代器上面的Dispose方法,则会执行Finally逻辑块。(注意,此时迭代器不会因此终止)。
我们可以理解为,只要对迭代器使用了Foreach语句,那么迭代器中的Finally语句将会按照正常的方式工作。
但是,如果没有调用Dispose方法,泛型迭代器将不会调用Finally逻辑块。我们写一个例子看看:
class Program
{
static void Main()
{
DateTime stop = DateTime.Now.AddSeconds(5);
foreach (int i in CountWithTimeLimit(stop))
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("得到了" + i);
if (i > 10)
{
Console.WriteLine("迭代器终止.....");
return;
}
Thread.Sleep(300);
}
}
static IEnumerable CountWithTimeLimit(DateTime limit)
{
try
{
for (int i = 1; i <= 100; i++)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("当前时间是" + DateTime.Now);
if (DateTime.Now > limit)
{
yield break;
}
yield return i;
}
}
finally
{
Console.WriteLine("迭代器finally块已经调用!");
}
}
}
那么我们看到的结果是:
如果我们不用foreach语句,而是使用for循环执行同样的逻辑,我们把上面主函数的逻辑注释掉,修改成自定义的for循环,即以下的样子:
static void Main()
{
DateTime stop = DateTime.Now.AddSeconds(5);
IEnumerable ienum = CountWithTimeLimit(stop);
IEnumerator ienumer = ienum.GetEnumerator();
for (int i = ienumer.Current; ; i = ienumer.Current)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("得到了" + i);
if (i > 10)
{
Console.WriteLine("迭代器终止");
return;
}
Thread.Sleep(300);
ienumer.MoveNext();
}
}
那么很显然,finally语句块将不会被调用。
但是,如果是非泛型迭代器,那么处于try逻辑块的yield return一定会执行finally逻辑块。需要注意的是,非泛型迭代器没有继承IDisposable接口。
同样的,C#的迭代器也需要我们注意以下几点: