C#中关键字 yield 的使用
C#中关键字 yield 的使用
1.背景知识点
(1)迭代器
也叫枚举器,是实现了接口 IEnumerator 的实例。它提供了一种方式以获取集合的下一个元素,进而允许“实现它的类或结构”可以遍历集合,并返回集合的元素。
接口 IEnumerable ,用于对外公开一个枚举器。
//枚举器 public interface IEnumerator { //获取枚举器当前位置的元素 object Current { get; } //将枚举器前进到集合的下一个元素: //如果枚举器已成功推进到下一个元素,返回 true; //如果枚举器已超过集合的末尾,返回 false bool MoveNext(); //将枚举器设置为其初始位置 void Reset(); } //对外公开一个枚举器 public interface IEnumerable { //获取枚举器 IEnumerator GetEnumerator(); } //支持泛型的枚举器 public interface IEnumerator<out T> : IEnumerator, IDisposable { //获取枚举器当前位置的元素 T Current { get; } } //对外公开一个支持泛型的枚举器 public interface IEnumerable<out T> : IEnumerable { //获取枚举器 IEnumerator<T> GetEnumerator(); } public interface IDisposable { //释放或重置非托管资源 void Dispose(); }
示例代码,用于实现一个迭代器 Test:包含一个字符串数组、一个索引,实现了属性 Current,以及实现了方法 MoveNext()、Reset()、Dispose()、GetEnumerator() 等。
public partial class Test : IEnumerable<string>, IEnumerator<string> { public Test() { Reset(); } private readonly string[] array = { " a", " b", " c" }; private int index; public string Current { get { return array[index]; } } object IEnumerator.Current { get { return array[index]; } } public bool MoveNext() { return ++index < array.Length; } public void Reset() { index = -1; } public void Dispose() { } public IEnumerator<string> GetEnumerator() { return this; } IEnumerator IEnumerable.GetEnumerator() { return this; } }
C# 语言已经内建了对迭代器的支持,相关的关键字有 foreach,yield 等。.NET 类库也提供了许多有用的迭代器,例如:List、Collection、Dictionary 等。
(2)foreach
使用 foreach 关键字可以遍历集合。在 foreach 语句,被遍历的对象必须实现 GetEnumerator() 方法,并且 public 以及返回类型 IEnumerator 或 IEnumerator<T>,否则将报错。
internal partial class Program { static void Main(string[] args) { Task.Run(() => { ToTest2(); }); Console.ReadKey(); } private static void ToTest2() { Test test = new Test(); //foreach foreach (var item in test) { Console.WriteLine(item); } } }
运行结果 :
它等效于以下代码:
private static void ToTest3() { Test test = new Test(); while (test.MoveNext()) { Console.WriteLine(test.Current); } }
2.关键字 yield
(1)用法
1)yield 必须结合 return 或者 break 一起使用:
- yield return [value]:在迭代中提供下一个值;
- yield break:显式表示迭代结束。
2)包含 yield return 的方法,它的返回类型必须是
- IEnumerable 或 IEnumerable<T>
- IEnumerator 或 IEnumerator<T>
- IAsyncEnumerable<T>
- IAsyncEnumerator<T>
关于接口 IAsyncEnumerable<T>、IAsyncEnumerator<T> 请参考:《C#中 IAsyncEnumerable 与 IAsyncEnumerator 的使用》
(2)原理
包含 yield return 的方法,会被编译器视为一个迭代器的实现。
这个迭代器的特性:
- 每执行到 yield return,视为迭代器 MoveNext() 返回 true;
每执行到 yield break,视为迭代器 MoveNext() 返回 false;
方法自动运行结束时,视为迭代器 MoveNext() 返回 false; - 将 yield return 后的 [value],组成一个“枚举”,
- 按语句: yield return [value]; 的执行顺序,依次返回 [value]。
对一个迭代器的实现,可以简化为以下实现:
开发者不用再创建元素集合、索引,不再实现属性 Current,以及不用实现方法 MoveNext()、Reset()、Dispose() 等,这些工作都交给了编译器,由编译器自动完成。
public class Test2 : IEnumerable<string> { public IEnumerator<string> Y() { yield return " a"; yield return " b"; yield return " c"; } public IEnumerator<string> GetEnumerator() { return this.Y(); } IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); } } internal partial class Program { static void Main(string[] args) { Task.Run(() => { ToTest3(); }); Console.ReadKey(); } private static void ToTest3() { Test2 test2 = new Test2(); //foreach foreach (var item in test2) { Console.WriteLine(item); } } }
甚至进一步简化为以下实现,连方法 GetEnumerator() 都不用实现,一并交给了编译器:
public class Test2 { public IEnumerable<string> Y() { yield return " a"; yield return " b"; yield return " c"; } } internal partial class Program { static void Main(string[] args) { Task.Run(() => { ToTest3(); }); Console.ReadKey(); } private static void ToTest3() { Test2 test2 = new Test2(); //foreach foreach (var item in test2.Y()) { Console.WriteLine(item); } } }
对简化实现的迭代器,仍然可以使用原始的方式进行调用:
private static void ToTest3() { Test2 test2 = new Test2(); var enumerator = test2.Y().GetEnumerator(); while (enumerator.MoveNext()) { Console.WriteLine(enumerator.Current); } }
(3)总结
实现一个“包含 yield return 的方法”,即是实现一个迭代器。
3.执行顺序
- “yield return 语句”的执行顺序 ,即是迭代器返回元素的顺序;
- “yield return 语句”并不是“return 语句”,因此,在它之后可以有其他语句;
- 每执行到“yield return 语句”时(相当于迭代器 MoveNext() 返回 true 时),则返回一个元素,程序将执行一次“ foreach 的循环体”;
执行一次“foreach 的循环体”之后,程序将从上一个“yield return 语句”开始,继续执行方法之后的代码; - 使用 “yield break 语句”,显式退出迭代,否则将等到方法结束时,自动结束迭代;
在“yield break 语句”之后的代码不被执行【“yield break 语句”才是类似“return 语句”的存在】;
显式退出迭代或自动结束迭代,相当于迭代器 MoveNext() 返回 false,不再执行“foreach 的循环体”。