🌴前言
在之前的 csharp 学习过程中学习了索引器, 这样我自定义类中的数据也就可以使用方括号的形式进行访问啦, 同时我还看到了一个 "迭代器" 的词汇, 这是啥东西啊? 网上一查, 标志性词汇是 "IEnumerable" 和 "IEnumerator"...这个我熟悉啊, Unity 的协程 'Coroutines' 技术中也使用到了这个词汇, 那赶紧看看其中的知识吧.
🍀C# 1.0 中的迭代器
C# 1.0 中, 迭代模式是通过两个接口实现的: IEnumerable 和 IEnumerator.
"正确实现了 IEnumerable 接口" 或者 "具有完全符合特征的方法" 的类型可以被迭代访问, 比如 C# 内置的数组, 链表类型, 这些都可以被迭代访问, 它们都实现了 IEnumerable 接口.
但是正确实现了 IEnumerable 接口的并不是迭代器, IEnumerable 接口中只有一个需要实现的方法 GetEnumerator(), 这个方法作用是会返回一个迭代器, 并不是实现一个迭代器.
- 正确实现了 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
|
public class CharList : IEnumerable { private readonly string charArray;
public CharList(string str) { charArray = str; }
public IEnumerator GetEnumerator() { return new CharEnumerator(charArray); } }
|
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; }
bool MoveNext(); void Reset(); >}
|
Current 属性. 必须是 public 修饰, 必须返回 object 类型, 必须实现 Get 器, 必须遵守的 Get 器规则: 获取当前索引位置的值.
MoveNext 方法. 必须是 public 修饰, 必须返回 bool 类型. 必须遵守的返回值规则: 如果可以获取下一个值, 返回 true, 如果无法或许下一个值, 则返回 false.
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
|
public class CharEnumerator : IEnumerator { private readonly string charArray;
private int currentIndex;
public CharEnumerator(string str) { currentIndex = -1; charArray = str; }
public object Current { get { return charArray[currentIndex]; } }
public bool MoveNext() { return ++currentIndex < charArray.Length; }
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 { private readonly string charArray;
public CharList(string str) { charArray = str; }
public IEnumerator GetEnumerator() { for (int i = 0; i < charArray.Length; i++) { yield return charArray[i]; } } }
|
🦄迭代器的执行顺序
将 C# 1.0 迭代器例子补全 (本文最后附有已补全的代码), 并在一个 foreach 中进行逐步调试就可以看到迭代访问时的代码执行顺序.
foreach (var item in charList)
charList: 调用 GetEnumerator 方法.
in: 调用 MoveNext 方法.
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(); 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++; } 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 所创建的迭代器的运行步骤如下.
直到第一次执行 MoveNext
方法时, 程序才会进入到 CreateEnumerable
方法中执行.
之后在方法 CreateEnumerable 中, 遇到 yield return
时会跳出方法, 回到之前的 MoveNext 处, 使 MoveNext 返回 true
, 并继续向下执行.
直到经由 while 循环再次遇到 MoveNext 方法时, 程序便会再次进入 CreateEnumerable 方法, 并且是从上次跳出方法的位置, 即 yield return 位置处继续向下执行 for 循环.
最后一次执行 MoveNext 方法时, 程序进入 CreateEnumerable 方法, 但是此时 CreateEnumerable 方法中已经没有可执行的 yield return 语句了, 于是运行完 CreateEnumerable 方法的最后一条语句后跳出 CreateEnumerable 方法, 使 MoveNext 返回 false
, 并继续向下执行.
遇到 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);
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 的运行.
🍒参考文献
🙄代码
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(); } }
public class CharList : IEnumerable { private readonly string charArray;
public CharList(string str) { charArray = str; }
public IEnumerator GetEnumerator() { return new CharEnumerator(charArray); } }
public class CharEnumerator : IEnumerator { private readonly string charArray;
private int currentIndex;
public CharEnumerator(string str) { currentIndex = -1; charArray = str; }
public object Current { get { return charArray[currentIndex]; } }
public bool MoveNext() { return ++currentIndex < charArray.Length; }
public void Reset() { currentIndex = -1; } } }
|