CSharp 迭代器

🌴前言

在之前的 csharp 学习过程中学习了索引器, 这样我自定义类中的数据也就可以使用方括号的形式进行访问啦, 同时我还看到了一个 "迭代器" 的词汇, 这是啥东西啊? 网上一查, 标志性词汇是 "IEnumerable" 和 "IEnumerator"...这个我熟悉啊, Unity 的协程 'Coroutines' 技术中也使用到了这个词汇, 那赶紧看看其中的知识吧.

🍀C# 1.0 中的迭代器

C# 1.0 中, 迭代模式是通过两个接口实现的: IEnumerableIEnumerator.

  1. "正确实现了 IEnumerable 接口" 或者 "具有完全符合特征的方法" 的类型可以被迭代访问, 比如 C# 内置的数组, 链表类型, 这些都可以被迭代访问, 它们都实现了 IEnumerable 接口.

    内置可被迭代类型

但是正确实现了 IEnumerable 接口的并不是迭代器, IEnumerable 接口中只有一个需要实现的方法 GetEnumerator(), 这个方法作用是会返回一个迭代器, 并不是实现一个迭代器.

  1. 正确实现了 IEnumerator 接口的类型才是迭代器.

C# 1.0 中如何自己实现一个可迭代访问的类型

C# 1.0 中, 想要实现一个可以被迭代访问的类型, 只要让这个类型正确实现 IEnumerable 接口即可. IEnumerable 接口中只有一个需要实现的方法 GetEnumerator(), 没有参数, 返回值类型是 IEnumerator. 比如实现一个可以迭代访问的 CharList 类型.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/// <summary>
/// 可迭代类型 CharList
/// </summary>
public class CharList : IEnumerable // 继承接口 IEnumerable
{
/// <summary>
/// 枚举数据
/// </summary>
private readonly string charArray;

/// <summary>
/// 构造器
/// </summary>
/// <param name="str">枚举数据</param>
public CharList(string str)
{
charArray = str;
}

/// <summary>
/// IEnumerable 中的 GetEnumerator 方法
/// </summary>
/// <returns></returns>
public IEnumerator GetEnumerator()
{
return new CharEnumerator(charArray); // new 一个迭代器并返回
}
}

C# 1.0 中如何自己实现一个迭代器类型

C# 1.0 中, 想要实现一个迭代器类型, 只要让这个类型正确实现 IEnumerator 接口即可. IEnumerator 接口中需要实现的内容有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
>public interface IEnumerator
>{
//
// 摘要:
// 获取集合中位于枚举数当前位置的元素。
//
// 返回结果:
// 集合中位于枚举数当前位置的元素。
object Current { get; }

//
// 摘要:
// 将枚举数推进到集合的下一个元素。
//
// 返回结果:
// 如果枚举数已成功地推进到下一个元素,则为 true;如果枚举数传递到集合的末尾,则为 false。
//
// 异常:
// T:System.InvalidOperationException:
// 创建枚举器后,已修改该集合。
bool MoveNext();
//
// 摘要:
// 将枚举数设置为其初始位置,该位置位于集合中第一个元素之前。
//
// 异常:
// T:System.InvalidOperationException:
// 创建枚举器后,已修改该集合。
void Reset();
>}
  1. Current 属性. 必须是 public 修饰, 必须返回 object 类型, 必须实现 Get 器, 必须遵守的 Get 器规则: 获取当前索引位置的值.

  2. MoveNext 方法. 必须是 public 修饰, 必须返回 bool 类型. 必须遵守的返回值规则: 如果可以获取下一个值, 返回 true, 如果无法或许下一个值, 则返回 false.

  3. Reset 方法. 必须是 public 修饰, 必须返回 void 类型. 必须遵守的逻辑规则: 将此时的索引位置设置为 第一个元素之前.

只有同时满足了上面全部要求, 才算是正确实现了一个迭代器.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/// <summary>
/// 迭代器 CharEnumerator
/// </summary>
public class CharEnumerator : IEnumerator // 继承 IEnumerator 接口
{
/// <summary>
/// 枚举数据
/// </summary>
private readonly string charArray;

/// <summary>
/// 索引位置
/// </summary>
private int currentIndex;

/// <summary>
/// 构造器
/// </summary>
/// <param name="str">枚举数据</param>
public CharEnumerator(string str)
{
currentIndex = -1; // 初始化索引位置
charArray = str; // 初始化枚举数据
}

/// <summary>
/// IEnumerator 中的 Current 属性
/// </summary>
public object Current
{
get
{
return charArray[currentIndex]; // 获取当前索引位置的值
}
}

/// <summary>
/// IEnumerator 中的 MoveNext 方法
/// </summary>
/// <returns>如果可以获取下一个值, 返回 true, 如果无法或许下一个值, 则返回 false.</returns>
public bool MoveNext()
{
return ++currentIndex < charArray.Length; // 如果 "索引位置" 自增后小于枚举数据长度, 说明可以获取下一个值, 返回 true, 否则返回 false.
}

/// <summary>
/// IEnumerator 中的 Reset 方法
/// </summary>
public void Reset()
{
currentIndex = -1; // 将索引位置设置为第一个元素之前
}
}

上面的代码段实现了一个最基础的迭代器, 这样 CharList 这个类型便可以使用 foreach 进行迭代访问了.

✨C# 2.0 中的迭代器

C# 2.0 中便可以使用 yield return 来简化迭代器的实现. 这样相比 1.0, 我们直接可以省略一个 CharEnumerator 类的实现, 方便了很多.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class CharList : IEnumerable
{
/// <summary>
/// 枚举数据
/// </summary>
private readonly string charArray;

/// <summary>
/// 构造器
/// </summary>
/// <param name="str">枚举数据</param>
public CharList(string str)
{
charArray = str;
}

/// <summary>
/// IEnumerable 中的 GetEnumerator 方法
/// </summary>
/// <returns></returns>
public IEnumerator GetEnumerator()
{
for (int i = 0; i < charArray.Length; i++)
{
yield return charArray[i]; // 使用 yield return 构造一个迭代器
}
}
}

🦄迭代器的执行顺序

将 C# 1.0 迭代器例子补全 (本文最后附有已补全的代码), 并在一个 foreach 中进行逐步调试就可以看到迭代访问时的代码执行顺序.

foreach (var item in charList)

  1. charList: 调用 GetEnumerator 方法.

  2. in: 调用 MoveNext 方法.

  3. item: 获取 Current 属性.

但是如果我们没有使用 C# 1.0 提供的形式实现迭代器, 而是使用了 C# 2.0 中的 yield return 实现迭代器, 那么此时迭代器的执行顺序又是什么呢?

我们来做一个例子测试一下, 先创建一个类 CreateEnumerable, 这个类可以返回一个可迭代器类型 IEnumerable<int>, 之后使用 GetEnumerator 方法获取迭代器, 然后使用 while 循环手动调用迭代器的 MoveNext 方法以及 Current 属性, 并输出执行顺序.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using System;
using System.Collections.Generic;

namespace Exercise
{
public class Program
{
static int runIndex = 0;
static readonly string Padding = new string('\t', 4);

static IEnumerable<int> CreateEnumerable()
{
Console.WriteLine("{0,3} : {1}Start of CreateEnumerable", runIndex, Padding); runIndex++;

for (int i = 0; i < 3; i++)
{
Console.WriteLine("{0,3} : {1}\tBefore yield {2}", runIndex, Padding, i); runIndex++;
yield return i;
Console.WriteLine("{0,3} : {1}\tAfter yield", runIndex, Padding); runIndex++;
}

Console.WriteLine("{0,3} : {1}Yielding final value", runIndex, Padding); runIndex++;
yield return -1;
Console.WriteLine("{0,3} : {1}End of CreateEnumerable()", runIndex, Padding); runIndex++;
}

private static void Main()
{
// 创建一个可以迭代访问的实例
IEnumerable<int> iterable = CreateEnumerable();

// 获取这个实例中的迭代器
IEnumerator<int> iterator = iterable.GetEnumerator();

Console.WriteLine("{0,3} : Starting to iterate", runIndex); runIndex++;

while (true)
{
Console.WriteLine("{0,3} : Calling MoveNext()...", runIndex); runIndex++;
bool result = iterator.MoveNext(); // 调用迭代器的 MoveNext 方法
Console.WriteLine("{0,3} : ...MoveNext result={1}", runIndex, result); runIndex++;
if (!result) break;
Console.WriteLine("{0,3} : Fetching Current...", runIndex); runIndex++;
Console.WriteLine("{0,3} : ...Current result={1}", runIndex, iterator.Current); runIndex++; // 获取迭代器的 Current 属性
}
Console.Read();
}
}
}

输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 0 : Starting to iterate
1 : Calling MoveNext()...
2 : Start of CreateEnumerable
3 : Before yield 0
4 : ...MoveNext result=True
5 : Fetching Current...
6 : ...Current result=0
7 : Calling MoveNext()...
8 : After yield
9 : Before yield 1
10 : ...MoveNext result=True
11 : Fetching Current...
12 : ...Current result=1
13 : Calling MoveNext()...
14 : After yield
15 : Before yield 2
16 : ...MoveNext result=True
17 : Fetching Current...
18 : ...Current result=2
19 : Calling MoveNext()...
20 : After yield
21 : Yielding final value
22 : ...MoveNext result=True
23 : Fetching Current...
24 : ...Current result=-1
25 : Calling MoveNext()...
26 : End of CreateEnumerable()
27 : ...MoveNext result=False

从输出结果中可以看出使用 yield return 所创建的迭代器的运行步骤如下.

  1. 直到第一次执行 MoveNext 方法时, 程序才会进入到 CreateEnumerable 方法中执行.

  2. 之后在方法 CreateEnumerable 中, 遇到 yield return 时会跳出方法, 回到之前的 MoveNext 处, 使 MoveNext 返回 true, 并继续向下执行.

  3. 直到经由 while 循环再次遇到 MoveNext 方法时, 程序便会再次进入 CreateEnumerable 方法, 并且是从上次跳出方法的位置, 即 yield return 位置处继续向下执行 for 循环.

  4. 最后一次执行 MoveNext 方法时, 程序进入 CreateEnumerable 方法, 但是此时 CreateEnumerable 方法中已经没有可执行的 yield return 语句了, 于是运行完 CreateEnumerable 方法的最后一条语句后跳出 CreateEnumerable 方法, 使 MoveNext 返回 false, 并继续向下执行.

  5. 遇到 break 跳出 while 循环, 程序结束.

从 CreateEnumerable 内部来看, yield return 相当于暂时退出了方法去执行另一段代码, 另一端代码执行完之后, 再回到 yield return 的位置继续执行.

yield break 退出迭代器

一般情况下, return 的作用是用于返回给调用者方法的结果或者结果一个方法的运行, 并在返回数值或者结束方法运行之前运行 finally 中的语句.

在返回值为 IEnumerable<> 类型的方法中, 如果想快速退出方法, 可以使用 yield break.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using System;
using System.Collections.Generic;

public class Program
{
public IEnumerable<int> CountWithTimeLimit(DateTime limit)
{
try
{
for (int i = 1; i <= 100; i++)
{
if (DateTime.Now >= limit)
{
yield break;
}

yield return i;
}
}
finally
{
Console.WriteLine("Finally: Stopping");
}
}

static void Main()
{
Program program = new Program();
DateTime stopTime = DateTime.Now.AddSeconds(2); // 获取 2 秒后的时间

Console.WriteLine("Start of Main");

foreach (int index in program.CountWithTimeLimit(stopTime))
{
Console.WriteLine("\tReceived {0}", index);

System.Threading.Thread.Sleep(300); // 毫秒
}

Console.WriteLine("End of Main");
Console.Read();
}
}

输出结果为:

1
2
3
4
5
6
7
8
9
10
Start of Main
Received 1
Received 2
Received 3
Received 4
Received 5
Received 6
Received 7
Finally: Stopping
End of Main

可以看出, yield return 只是暂时离开方法, 到另一个位置执行其他的代码, 并不会执行 finally 语句块, 而 yield break 则直接转而执行了 finally 语句块中的语句, 并彻底结束了 foreach 的运行.

🍒参考文献

  • C#迭代器

🙄代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
using System;
using System.Collections;

namespace Exercise
{
class Program
{
static void Main()
{
CharList charList = new CharList("Hello World");

foreach (var item in charList)
{
Console.Write(item);
}

Console.ReadKey();
}
}

/// <summary>
/// 可迭代类型 CharList
/// </summary>
public class CharList : IEnumerable // 继承接口 IEnumerable
{
/// <summary>
/// 枚举数据
/// </summary>
private readonly string charArray;

/// <summary>
/// 构造器
/// </summary>
/// <param name="str">枚举数据</param>
public CharList(string str)
{
charArray = str;
}

/// <summary>
/// IEnumerable 中的 GetEnumerator 方法
/// </summary>
/// <returns></returns>
public IEnumerator GetEnumerator()
{
return new CharEnumerator(charArray); // new 一个迭代器并返回
}
}

/// <summary>
/// 迭代器 CharEnumerator
/// </summary>
public class CharEnumerator : IEnumerator // 继承 IEnumerator 接口
{
/// <summary>
/// 枚举数据
/// </summary>
private readonly string charArray;

/// <summary>
/// 索引位置
/// </summary>
private int currentIndex;

/// <summary>
/// 构造器
/// </summary>
/// <param name="str">枚举数据</param>
public CharEnumerator(string str)
{
currentIndex = -1; // 初始化索引位置
charArray = str; // 初始化枚举数据
}

/// <summary>
/// IEnumerator 中的 Current 属性
/// </summary>
public object Current
{
get
{
return charArray[currentIndex]; // 获取当前索引位置的值
}
}

/// <summary>
/// IEnumerator 中的 MoveNext 方法
/// </summary>
/// <returns>如果可以获取下一个值, 返回 true, 如果无法或许下一个值, 则返回 false.</returns>
public bool MoveNext()
{
return ++currentIndex < charArray.Length; // 如果 "索引位置" 自增后小于枚举数据长度, 说明可以获取下一个值, 返回 true, 否则返回 false.
}

/// <summary>
/// IEnumerator 中的 Reset 方法
/// </summary>
public void Reset()
{
currentIndex = -1; // 将索引位置设置为第一个元素之前
}
}
}