枚举器和可枚举类型 使用foreach语句
foreach 作用 :用来遍历数组等可枚举集合 ,依次读取每一个元素,迭代变量(iteration variable)只能读取,不能修改 。
数组属于可枚举类型 ,具备GetEnumerator()方法;调用该方法会返回一个枚举器(enumerator)对象 。
可枚举类型 (enumerable type):拥有GetEnumerator()方法,产出枚举器实例。
枚举器 (enumerator):记录集合遍历位置,可以依次返回集合内的每一项元素。
1 2 3 4 int [] arr1 = { 10 , 11 , 12 , 13 }; foreach (int item in arr1) Console.WriteLine($"Item value: { item } " );
1 2 3 4 Item value: 10 Item value: 11 Item value: 12 Item value: 13
foreach结构设计用来和可枚举类型一起使用,当foreach作用在可枚举对象上时:
调用对象的GetEnumerator()拿到枚举器;
通过枚举器逐个获取集合元素,赋值给迭代变量;
迭代变量仅可读,不允许修改集合元素。
1 2 3 4 5 foreach ( Type VarName in EnumerableObject ){ … }
IEnumerator接口 实现IEnumerator接口的枚举器 包含3个函数成员:Current、MoveNext以及Reset。
Current是返回序列中当前位置项的属性。
它是只读属性。
它返回object类型的引用,所以可以返回任何类型的对象。
MoveNext是把枚举器位置前进到集合中下一项的方法。返回布尔值,指示新的位置是有效位置还是已经超过了序列的尾部。
如果新的位置是有效的,方法返回true。
如果新的位置是无效的(比如当前位置到达了尾部),方法返回false。
枚举器的原始位置在序列中的第一项之前,因此MoveNext必须在第一次使用Current之前调用 。
Reset是把位置重置为原始状态的方法。 枚举器跟踪序列中当前项的方式完全取决于实现。可以通过对象引用、索引值或其他方式来实现。对于内置的一维数组来说,就使用项的索引。
1 2 3 4 5 6 7 8 9 10 static void Main (){ int [] arr1 = { 10 , 11 , 12 , 13 }; IEnumerator ie = arr1.GetEnumerator(); while ( ie.MoveNext() ) { int item = (int ) ie.Current; Console.WriteLine($"Item value: { item } " ); } }
1 2 3 4 Item value: 10 Item value: 11 Item value: 12 Item value: 13
IEnumerable接口 可枚举类 是指实现了IEnumerable接口的类。IEnumerable接口只有一个成员——GetEnumerator方法,它返回对象的枚举器。IEnumerable基础框架
1 2 3 4 5 6 7 8 9 using System.Collections;class MyClass : IEnumerable { public IEnumerator GetEnumerator { ... } ... }
使用IEnumerable和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 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 using System;using System.Collections;class ColorEnumerator : IEnumerator { string [] colors; int position = -1 ; public ColorEnumerator ( string [] theColors ) { colors = new string [theColors.Length]; for ( int i = 0 ; i < theColors.Length; i++ ) colors[i] = theColors[i]; } public object Current { get { if ( position == -1 ) throw new InvalidOperationException(); if ( position >= colors.Length ) throw new InvalidOperationException(); return colors[position]; } } public bool MoveNext () { if ( position < colors.Length - 1 ) { position++; return true ; } else return false ; } public void Reset () { position = -1 ; } } class Spectrum : IEnumerable { string [] Colors = { "violet" , "blue" , "cyan" , "green" , "yellow" , "orange" , "red" }; public IEnumerator GetEnumerator () { return new ColorEnumerator( Colors ); } } class Program { static void Main () { Spectrum spectrum = new Spectrum(); foreach ( string color in spectrum ) { Console.WriteLine( color ); } } }
1 2 3 4 5 6 7 violet blue cyan green yellow orange red
泛型枚举接口 目前我们描述的枚举接口都是非泛型版本。实际上,在大多数情况下你应该使用泛型版本 IEnumerable<T> 和 IEnumerator<T>。 两者之间的本质差别如下所示。
对于非泛型接口形式:
IEnumerable接口的GetEnumerator方法返回实现IEnumerator的枚举器类实例;
实现IEnumerator的类实现了Current属性,它返回object类型的引用,然后我们必须把它转化为对象的实际类型。
泛型接口继承自非泛型接口。对于泛型接口形式:
IEnumerable<T>接口的GetEnumerator方法返回实现IEnumerator<T>的枚举器类的实例;
实现IEnumerator<T>的类实现了Current属性,它返回实际类型的实例,而不是object基类的引用。
这些是协变接口,所以它们的实际声明就是IEnumerable<out T>和IEnumerator<out T>。这意味着实现这些接口的对象可以是派生的类型。
需要重点注意的是,目前所看到的非泛型接口的实现不是类型安全的,它们返回object类型的引用,然后必须转化为实际类型。 非泛型版本可用于C#2.0以前没有泛型的遗留代码。 而泛型接口的枚举器是类型安全的,它返回实际类型的引用。如果要创建自己的可枚举类,应该实现这些泛型接口。
迭代器
迭代器(iterator) :C#2.0引入,使用yield return,编译器自动生成底层枚举器类 ,不用手动手写实现IEnumerator<T>,极大简化自定义枚举器。
yield return:标记序列里的下一个返回元素 ,不是普通return,不会终止整个方法。
延迟执行(核心) :
调用迭代器方法时,代码不会立刻执行 ;
只有调用MoveNext()的时候,方法才执行,执行到下一个yield return就暂停,保存当前状态;
下一次MoveNext(),从上次暂停位置继续往下跑。
两种写法效果等价:
直接写多条yield return 常量;
在循环内部写yield return 变量。
返回值类型:迭代器方法返回IEnumerator<T>(枚举器),也可以返回IEnumerable<T>(可枚举序列)。
和手动实现枚举器对比:
手动写要维护位置、MoveNext、Current;
yield迭代器由编译器自动生成状态机,开发者只关心业务逻辑。
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 using System;using System.Collections.Generic;class Demo { public IEnumerator<string > BlackAndWhite () { yield return "black" ; yield return "gray" ; yield return "white" ; } public IEnumerator<string > BlackAndWhite () { string [] theColors = { "black" , "gray" , "white" }; for (int i = 0 ; i < theColors.Length; i++) yield return theColors[i]; } static void Main () { Demo d = new Demo(); var enumerator = d.BlackAndWhite(); while (enumerator.MoveNext()) { Console.WriteLine(enumerator.Current); } } }
迭代器块
迭代器块位置 :可以写在方法、访问器、运算符 的主体内部,代码里必须包含yield语句。
执行模型区别
普通代码块:命令式,从上到下完整顺序执行。
迭代器块:声明性,不一次性全部执行 ,由编译器自动生成嵌套的枚举器类,描述元素如何产出。
两个核心 yield 语句
yield return:产出序列里的下一个元素,暂停执行,下次迭代继续往下跑。
yield break:终止迭代,不再返回元素,等价于迭代结束。
编译器会根据迭代器块自动生成实现IEnumerator的嵌套枚举器类,不用手写枚举器。
迭代器是延迟执行 ,foreach 遍历的时候才会真正执行迭代器内部代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 using System;using System.Collections.Generic;class Demo { public IEnumerable<int > GetNumbers () { yield return 1 ; yield return 2 ; yield return 3 ; yield break ; } static void Main () { Demo d = new Demo(); foreach (var num in d.GetNumbers()) { Console.WriteLine(num); } } }
使用迭代器来创建枚举器
迭代器方法 :BlackAndWhite() 是迭代器块,内部使用yield return产出序列元素,编译器自动生成枚举器对象。
可枚举类型要求 :类只要拥有返回IEnumerator的GetEnumerator()方法,就可以被foreach遍历,不一定必须显式写IEnumerable接口声明,编译器只识别这个方法实现。
调用关系 :GetEnumerator()直接调用迭代器方法,把迭代器生成的枚举器返回给foreach使用。
延迟执行 :执行foreach迭代的时候,才会一步步执行迭代器内部每一条yield return,依次输出元素。
迭代器简化开发:不用手动手写实现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 using System;using System.Collections.Generic;class MyClass { public IEnumerator<string > GetEnumerator () { return BlackAndWhite(); } public IEnumerator<string > BlackAndWhite () { yield return "black" ; yield return "gray" ; yield return "white" ; } } class Program { static void Main () { MyClass mc = new MyClass(); foreach (string shade in mc) { Console.WriteLine(shade); } } }
使用迭代器来创建可枚举类型
两种迭代器返回类型区分
返回 IEnumerator<T>:迭代器直接产出枚举器 ,只能用于类内部GetEnumerator返回。
返回 IEnumerable<T>:迭代器产出可枚举类型 ,既可以内部供GetEnumerator()调用,也可以外部直接拿来做foreach遍历。
调用流程:当迭代器返回IEnumerable<T>时,GetEnumerator()先调用迭代器拿到可枚举对象,再调用可枚举对象的.GetEnumerator()拿到真正枚举器。
使用两种遍历写法:
foreach(… in mc):使用类实例,走类本身的GetEnumerator()。
foreach(… in mc.BlackAndWhite()):直接调用迭代器方法,使用它返回的可枚举对象。
编译器处理:无论返回IEnumerable<T>还是IEnumerator<T>,编译器都会自动生成对应的嵌套类;返回IEnumerable<T>时,生成的可枚举类每次调用GetEnumerator()都会生成全新枚举器,可以多次重复迭代 。
本例输出两份序列,因为执行了两次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 using System;using System.Collections.Generic;class MyClass { public IEnumerator<string > GetEnumerator () { IEnumerable<string > myEnumerable = BlackAndWhite(); return myEnumerable.GetEnumerator(); } public IEnumerable<string > BlackAndWhite () { yield return "black" ; yield return "gray" ; yield return "white" ; } } class Program { static void Main () { MyClass mc = new MyClass(); foreach (string shade in mc) { Console.Write($"{shade} " ); } foreach (string shade in mc.BlackAndWhite()) { Console.Write($"{shade} " ); } } }
1 black gray white black gray white
常见迭代器模式
迭代器两种返回类型
IEnumerator<T>:迭代器产出枚举器 。
类必须实现GetEnumerator() ,把迭代器返回的枚举器返回出去,类实例才可以foreach。
迭代器方法本身不能直接用于foreach。
IEnumerable<T>:迭代器产出可枚举对象 。
可选①:类实现GetEnumerator():调用迭代器拿到可枚举对象,再调用它的.GetEnumerator()拿到枚举器;此时类实例支持foreach。
可选②:不实现GetEnumerator() :类本身实例不能直接foreach,但直接调用迭代器方法 得到可枚举对象,依然可以foreach遍历。
核心区分
IEnumerator:负责遍历状态(MoveNext、Current)。
IEnumerable:负责生产出新枚举器,可以多次重复迭代 。
开发选择:想要类实例支持foreach,就要提供GetEnumerator();只想对外提供一组序列,不要求类本身可枚举,迭代器返回IEnumerable<T>,外部直接调用迭代器方法即可。 ![[csharp-16-yield-1.png]]
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 using System;using System.Collections.Generic;class Demo_EnumeratorReturn { public IEnumerator<string > GetEnumerator () { return IteratorMethod_Enum(); } public IEnumerator<string > IteratorMethod_Enum () { yield return "A" ; yield return "B" ; } } class Demo_EnumerableReturn { public IEnumerator<string > GetEnumerator () { IEnumerable<string > enumerableObj = IteratorMethod_Enumable(); return enumerableObj.GetEnumerator(); } public IEnumerable<string > IteratorMethod_Enumable () { yield return "X" ; yield return "Y" ; } } class Demo_NoGetEnumerator { public IEnumerable<string > IteratorMethod_Enumable () { yield return "M" ; yield return "N" ; } } class Program { static void Main () { Demo_EnumeratorReturn de = new Demo_EnumeratorReturn(); foreach (var s in de) { Console.Write(s + " " ); } Console.WriteLine(); Demo_EnumerableReturn den = new Demo_EnumerableReturn(); foreach (var s in den) { Console.Write(s + " " ); } Console.WriteLine(); foreach (var s in den.IteratorMethod_Enumable()) { Console.Write(s + " " ); } Console.WriteLine(); Demo_NoGetEnumerator dn = new Demo_NoGetEnumerator(); foreach (var s in dn.IteratorMethod_Enumable()) { Console.Write(s + " " ); } } }
产生多个可枚举类型
Spectrum没有实现GetEnumerator() ,因此类实例本身不能直接放在foreach中使用(foreach(var x in spectrum)编译报错),类本身不是可枚举类型。
类中可以定义多个迭代器方法 ,每个迭代器返回IEnumerable<T>可枚举对象,对外提供不同的遍历序列(本例正序、逆序两套序列)。
使用方式:foreach直接调用迭代器方法spectrum.UVtoIR(),使用方法返回的可枚举对象完成遍历。
yield return可以写在for循环内部,循环迭代时逐个产出元素,属于延迟执行;每次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 using System;using System.Collections.Generic;class Spectrum { string [] colors = { "violet" , "blue" , "cyan" , "green" , "yellow" , "orange" , "red" }; public IEnumerable<string > UVtoIR () { for (int i=0 ; i < colors.Length; i++) { yield return colors[i]; } } public IEnumerable<string > IRtoUV () { for (int i=colors.Length - 1 ; i >= 0 ; i--) { yield return colors[i]; } } } class Program { static void Main () { Spectrum spectrum = new Spectrum(); foreach (string color in spectrum.UVtoIR()) { Console.Write($"{color} " ); } Console.WriteLine(); foreach (string color in spectrum.IRtoUV()) { Console.Write($"{color} " ); } Console.WriteLine(); } }
1 2 violet blue cyan green yellow orange red red orange yellow green cyan blue violet
将迭代器作为属性
迭代器可以写在属性的get访问器中 ,不只是只能写在方法里;属性返回类型为IEnumerator<T>,yield return写在get{ }内部。
本类实现了GetEnumerator(),类本身是可枚举类型 ,可以直接foreach (var x in spectrumInstance)。
GetEnumerator()利用三元运算符,根据对象内部布尔标记,动态选择返回两个迭代器属性之一。
对象在构造的时候就确定遍历方向,同一个对象固定一套遍历顺序。
返回IEnumerator<T>迭代器(属性版本),产出枚举器;这里不能直接foreach(spectrum.UVtoIR),因为UVtoIR是枚举器IEnumerator<T>,不是可枚举IEnumerable<T>。
对比上一版:
上例:多个IEnumerable< T>迭代器方法 ,类本身不可枚举,外部直接调用方法foreach。
本例:多个IEnumerator< T>迭代器属性 ,类本身可枚举;由GetEnumerator()运行时选择具体使用哪一套枚举逻辑。
延迟执行依然生效:每次foreach触发,迭代器重新执行循环,逐个yield产出元素。
小提示:迭代器属性返回IEnumerator<T>,一般只用于类内部GetEnumerator调用;对外给外部foreach使用,优先使用返回IEnumerable<T>的迭代器(方法)。
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 using System;using System.Collections.Generic;class Spectrum { private bool _listFromUVtoIR; string [] colors = { "violet" , "blue" , "cyan" , "green" , "yellow" , "orange" , "red" }; public Spectrum (bool listFromUVtoIR ) { _listFromUVtoIR = listFromUVtoIR; } public IEnumerator<string > GetEnumerator () { return _listFromUVtoIR ? UVtoIR : IRtoUV; } public IEnumerator<string > UVtoIR { get { for (int i = 0 ; i < colors.Length; i++) { yield return colors[i]; } } } public IEnumerator<string > IRtoUV { get { for (int i = colors.Length - 1 ; i >= 0 ; i--) { yield return colors[i]; } } } } class Program { static void Main () { Spectrum startUV = new Spectrum(true ); Spectrum startIR = new Spectrum(false ); foreach (string color in startUV) { Console.Write($"{color} " ); } Console.WriteLine(); foreach (string color in startIR) { Console.Write($"{color} " ); } Console.WriteLine(); } }
1 2 violet blue cyan green yellow orange red red orange yellow green cyan blue violet
迭代器的实质
命名空间 :使用迭代器/yield,必须引入using System.Collections.Generic;。
Reset 方法 :编译器自动生成的迭代器枚举器,Reset () 方法只是为了满足接口,实际调用直接抛NotSupportedException,不能用来重置迭代器状态 ;想要从头遍历,需要重新获取新枚举器。
编译器把 yield 迭代器编译为四状态状态机 :
Before:初始,未调用过MoveNext()
Running:正在执行迭代器代码,执行yield return/yield break/方法结束就退出Running
Suspended:暂停,等待下一次MoveNext()恢复执行
After:迭代全部完成,无更多元素
状态流转:调用MoveNext() → 进入 Running;
还有元素:切到Suspended暂停,保存现场;
没有元素:切到After结束。
foreach 每一轮本质就是调用MoveNext()驱动状态机在各个状态之间切换,实现暂停‑恢复执行,这就是yield延迟执行的底层原理。 ![[csharp-16-yield-2.png]]