July 23, 2023

迭代器

迭代器是一种对象,可以视为是一个集合的书签或者游标,表示一个集合中的某个位置,用以遍历集合中的部分或全部元素,它提供了一个方法顺序访问一个集合中的各个元素,而不暴露其内部表示,而C#已经为我们提供了几个迭代器的接口

枚举接口

枚举接口IEnumerable和IEnumerator​​​是迭代器模式(iterator pattern)​在C#中的实现​​​。它们实现在集合上进行简单迭代的效果

IEnumerable接口

集合需要实现IEnumerable接口,才能被迭代,这个接口实现GetEnumerator方法,这个方法会返回一个IEnumerator对象,这就是我们的迭代器对象

IEnumerator接口

迭代器实现IEnumerator接口,这个接口有三个方法

通过自定义类来实现枚举接口的功能

集合类

1
2
3
4
5
6
7
8
9
public class GameEngineEnumerable : IEnumerable  
{
private string[] _gameEngines = new string[5] { "Unity", "Godot", "GameMakerStudio", "Cocos", "RPGMaker" };

public IEnumerator GetEnumerator()
{
return new GameEngineEnumerator(_gameEngines);
}
}

迭代器类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class GameEngineEnumerator : IEnumerator  
{
private string[] _gameEngines;
private int _position = -1; //索引

public GameEngineEnumerator(string[] gameEngines)
{
_gameEngines = new string[gameEngines.Length];
for (int i = 0; i < gameEngines.Length; i++)
{
_gameEngines[i] = gameEngines[i];
}
}
public bool MoveNext()
{
}
public void Reset()
{
}
public object Current { get; }
}

接下来实现 IEnumerator接口的三个方法

1
2
3
4
5
6
7
8
9
public bool MoveNext()  
{
if (_position < _gameEngines.Length)
{
_position++;
return true;
}
return false;
}
1
2
3
4
5
6
7
8
9
10
public object Current  
{
get
{
//判断越界情况
if (_position >= _gameEngines.Length)
{ return null;
}
return _gameEngines[_position];
}}
1
2
3
4
public void Reset()  
{
_position = -1;
}

接下来,我们在主函数中实现遍历

1
2
3
4
5
6
7
8
9
public static void Main(string[] args)  
{
GameEngineEnumerable gameEngine = new GameEngineEnumerable();
IEnumerator gameEngineEnumerator = gameEngine.GetEnumerator();
while (gameEngineEnumerator.MoveNext())
{
Console.WriteLine(gameEngineEnumerator.Current);
}
}

成功输出string数组里面的值

使用yield语句简化迭代

我们把包含 yield 语句的方法或属性称为迭代块
按照以上的方式实现迭代显然太过繁琐,C#为我们提供了yield语句简化了这个过程,我们可以使用yield语句提供下一个值(yield return)或者表示迭代结束(yield break)

现在,我们注释掉GameEngineEnumerator类,将GetEnumerator方法改为以下代码

1
2
3
4
5
6
7
8
public IEnumerator GetEnumerator()  
{
//return new GameEngineEnumerator(_gameEngines);
for (int i = 0; i < _gameEngines.Length; i++)
{
yield return _gameEngines[i];
}
}

一样能实现相同的效果,但代码得到了极大的简化
而当我们执行yield break时,迭代器立即停止,MoveNext立即返回false,Current停留在最后一次保留的值上
在yield return执行完毕以后,函数并不会销毁,而是“休克”,等待返回值的IEnumerator执行下一次MoveNext(),yield return语句指定了枚举器中下一个可枚举项,迭代器在每次调用MoveNext函数时,会顺着上一次的枚举项(yield return)按照我们自己写的逻辑执行到下一个枚举项去

foreach

在C#中,我们可以使用foreach语句遍历可枚举集合,例如数组,哈希表,字典等,例如

1
2
3
4
foreach (var item in str)
{
Console.WriteLine(item);
}

而事实上,foreach内部就是由迭代器实现的,运行时并不直接支持 foreach 语句,C# 编译器会转换代码(foreach也类似于一种语法糖)
以上为官方的foreach写法,下面我们手动实现一个民间foreach

1
2
3
4
5
6
7
8
void ForeachFunc(string str)  
{
IEnumerator e = str.GetEnumerator();
while(e.MoveNext())
{
Console.WriteLine(e.Current);
}
}

我们调用这个方法,会发现输出的结果是一样的,实际上,foreach的实现其实非常简单,就类似于上面的民间foreach,对集合调用GetEnumerator方法获取迭代器(string继承了IEnumerable接口),然后使用while循环,直到MoveNext方法返回false,而在循环的过程中,可以调用Current属性,获取迭代器当前所对应的值

Unity中的协程

Unity中的协程正是由迭代器实现的,具体内容,请转到[[协程原理的简单认识]]

About this Post

This post is written by Yuan, licensed under CC BY-NC 4.0.

#C#