枚举器和可枚举类型

使用foreach语句

  1. foreach 作用:用来遍历数组等可枚举集合,依次读取每一个元素,迭代变量(iteration variable)只能读取,不能修改。
  2. 数组属于可枚举类型,具备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
  1. foreach结构设计用来和可枚举类型一起使用,当foreach作用在可枚举对象上时:
    • 调用对象的GetEnumerator()拿到枚举器;
    • 通过枚举器逐个获取集合元素,赋值给迭代变量;
    • 迭代变量仅可读,不允许修改集合元素。
1
2
3
4
5
// EnumerableObject 必须是可枚举类型
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;

//实现 IEnumerable 接口
class MyClass : IEnumerable
{
// 返回 IEnumerator 类型的对象
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;

// 自定义枚举器,实现IEnumerator接口
class ColorEnumerator : IEnumerator
{
string[] colors;
int position = -1; // 枚举器初始位置 -1

//构造函数
public ColorEnumerator( string[] theColors )
{
colors = new string[theColors.Length];
for ( int i = 0; i < theColors.Length; i++ )
colors[i] = theColors[i];
}

//实现 Current 属性
public object Current
{
get
{
if ( position == -1 )
throw new InvalidOperationException();
if ( position >= colors.Length )
throw new InvalidOperationException();
return colors[position];
}
}

//实现 MoveNext
public bool MoveNext()
{
if ( position < colors.Length - 1 )
{
position++;
return true;
}
else
return false;
}

//实现 Reset
public void Reset()
{
position = -1;
}
}
//可枚举类,实现IEnumerable
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以前没有泛型的遗留代码。
而泛型接口的枚举器是类型安全的,它返回实际类型的引用。如果要创建自己的可枚举类,应该实现这些泛型接口。

迭代器

  1. 迭代器(iterator):C#2.0引入,使用yield return,编译器自动生成底层枚举器类,不用手动手写实现IEnumerator<T>,极大简化自定义枚举器。
  2. yield return:标记序列里的下一个返回元素,不是普通return,不会终止整个方法。
  3. 延迟执行(核心):
    • 调用迭代器方法时,代码不会立刻执行;
    • 只有调用MoveNext()的时候,方法才执行,执行到下一个yield return就暂停,保存当前状态;
    • 下一次MoveNext(),从上次暂停位置继续往下跑。
  4. 两种写法效果等价:
    • 直接写多条yield return 常量;
    • 在循环内部写yield return 变量。
  5. 返回值类型:迭代器方法返回IEnumerator<T>(枚举器),也可以返回IEnumerable<T>(可枚举序列)。
  6. 和手动实现枚举器对比:
    • 手动写要维护位置、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
{
//版本1:直接多条 yield return
public IEnumerator<string> BlackAndWhite()
{
yield return "black"; //yield return
yield return "gray"; //yield return
yield return "white"; //yield return
}
//版本2
public IEnumerator<string> BlackAndWhite()
{
string[] theColors = { "black", "gray", "white" };
for (int i = 0; i < theColors.Length; i++)
yield return theColors[i]; //yield return
}
static void Main()
{
Demo d = new Demo();
// 使用迭代器返回的枚举器遍历
var enumerator = d.BlackAndWhite();
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}
}
}
1
2
3
black
gray
white

迭代器块

  1. 迭代器块位置:可以写在方法、访问器、运算符的主体内部,代码里必须包含yield语句。
  2. 执行模型区别
    • 普通代码块:命令式,从上到下完整顺序执行。
    • 迭代器块:声明性,不一次性全部执行,由编译器自动生成嵌套的枚举器类,描述元素如何产出。
  3. 两个核心 yield 语句
    • yield return:产出序列里的下一个元素,暂停执行,下次迭代继续往下跑。
    • yield break:终止迭代,不再返回元素,等价于迭代结束。
  4. 编译器会根据迭代器块自动生成实现IEnumerator的嵌套枚举器类,不用手写枚举器。
  5. 迭代器是延迟执行,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
{
// 迭代器块:方法主体,使用 yield return / yield break
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);
}
}
}
1
2
3
1
2
3

使用迭代器来创建枚举器

  1. 迭代器方法:BlackAndWhite() 是迭代器块,内部使用yield return产出序列元素,编译器自动生成枚举器对象。
  2. 可枚举类型要求:类只要拥有返回IEnumerator的GetEnumerator()方法,就可以被foreach遍历,不一定必须显式写IEnumerable接口声明,编译器只识别这个方法实现。
  3. 调用关系:GetEnumerator()直接调用迭代器方法,把迭代器生成的枚举器返回给foreach使用。
  4. 延迟执行:执行foreach迭代的时候,才会一步步执行迭代器内部每一条yield return,依次输出元素。
  5. 迭代器简化开发:不用手动手写实现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
{
// 实现GetEnumerator,满足foreach可枚举要求
public IEnumerator<string> GetEnumerator()
{
return BlackAndWhite(); // 返回迭代器生成的枚举器
}

// 迭代器块方法,返回IEnumerator<string>
public IEnumerator<string> BlackAndWhite()
{
yield return "black"; // 返回第一个元素
yield return "gray"; // 返回第二个元素
yield return "white"; // 返回第三个元素
}
}

class Program
{
static void Main()
{
MyClass mc = new MyClass();
// MyClass实现GetEnumerator,可以直接在foreach使用实例
foreach (string shade in mc)
{
Console.WriteLine(shade);
}
}
}
1
2
3
black
gray
white

使用迭代器来创建可枚举类型

  1. 两种迭代器返回类型区分
    • 返回 IEnumerator<T>:迭代器直接产出枚举器,只能用于类内部GetEnumerator返回。
    • 返回 IEnumerable<T>:迭代器产出可枚举类型,既可以内部供GetEnumerator()调用,也可以外部直接拿来做foreach遍历。
  2. 调用流程:当迭代器返回IEnumerable<T>时,GetEnumerator()先调用迭代器拿到可枚举对象,再调用可枚举对象的.GetEnumerator()拿到真正枚举器。
  3. 使用两种遍历写法:
    • foreach(… in mc):使用类实例,走类本身的GetEnumerator()。
    • foreach(… in mc.BlackAndWhite()):直接调用迭代器方法,使用它返回的可枚举对象。
  4. 编译器处理:无论返回IEnumerable<T>还是IEnumerator<T>,编译器都会自动生成对应的嵌套类;返回IEnumerable<T>时,生成的可枚举类每次调用GetEnumerator()都会生成全新枚举器,可以多次重复迭代。
  5. 本例输出两份序列,因为执行了两次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)
IEnumerable<string> myEnumerable = BlackAndWhite();
// 在可枚举对象上调用GetEnumerator拿到枚举器返回
return myEnumerable.GetEnumerator();
}

// 迭代器,返回IEnumerable可枚举类型(不是IEnumerator)
public IEnumerable<string> BlackAndWhite()
{
yield return "black";
yield return "gray";
yield return "white";
}
}

class Program
{
static void Main()
{
MyClass mc = new MyClass();

// 方式1:直接使用MyClass实例做foreach,依赖类的GetEnumerator
foreach (string shade in mc)
{
Console.Write($"{shade} ");
}

// 方式2:直接调用迭代器方法,返回IEnumerable,直接foreach遍历
foreach (string shade in mc.BlackAndWhite())
{
Console.Write($"{shade} ");
}
}
}
1
black gray white black gray white

常见迭代器模式

  1. 迭代器两种返回类型
    1. IEnumerator<T>:迭代器产出枚举器。
      • 类必须实现GetEnumerator(),把迭代器返回的枚举器返回出去,类实例才可以foreach。
      • 迭代器方法本身不能直接用于foreach。
    2. IEnumerable<T>:迭代器产出可枚举对象。
      • 可选①:类实现GetEnumerator():调用迭代器拿到可枚举对象,再调用它的.GetEnumerator()拿到枚举器;此时类实例支持foreach。
      • 可选②:不实现GetEnumerator():类本身实例不能直接foreach,但直接调用迭代器方法得到可枚举对象,依然可以foreach遍历。
  2. 核心区分
    • IEnumerator:负责遍历状态(MoveNext、Current)。
    • IEnumerable:负责生产出新枚举器,可以多次重复迭代。
  3. 开发选择:想要类实例支持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;

// 模式1:迭代器返回 IEnumerator<T>(返回枚举器)
class Demo_EnumeratorReturn
{
// 必须实现GetEnumerator,类实例才能foreach
public IEnumerator<string> GetEnumerator()
{
return IteratorMethod_Enum(); // 直接返回迭代器产出的枚举器
}

// 迭代器:返回 IEnumerator<string> 枚举器
public IEnumerator<string> IteratorMethod_Enum()
{
yield return "A";
yield return "B";
}
}

// 模式2:迭代器返回 IEnumerable<T>(返回可枚举类型)
class Demo_EnumerableReturn
{
// 可选:实现GetEnumerator,则类实例支持foreach
public IEnumerator<string> GetEnumerator()
{
IEnumerable<string> enumerableObj = IteratorMethod_Enumable();
// 从可枚举对象上拿到枚举器返回
return enumerableObj.GetEnumerator();
}

// 迭代器:返回 IEnumerable<string> 可枚举对象
public IEnumerable<string> IteratorMethod_Enumable()
{
yield return "X";
yield return "Y";
}
}

// 模式3:不实现GetEnumerator,类本身不可foreach,直接调用迭代器方法
class Demo_NoGetEnumerator
{
// 只有迭代器方法,类没有GetEnumerator,类实例不能直接foreach
public IEnumerable<string> IteratorMethod_Enumable()
{
yield return "M";
yield return "N";
}
}

class Program
{
static void Main()
{
// ==========模式1:返回IEnumerator<T>==========
Demo_EnumeratorReturn de = new Demo_EnumeratorReturn();
foreach(var s in de)
{
Console.Write(s + " "); // A B
}
Console.WriteLine();

// ==========模式2:返回IEnumerable<T>,类实现GetEnumerator==========
Demo_EnumerableReturn den = new Demo_EnumerableReturn();
foreach(var s in den)
{
Console.Write(s + " "); // X Y
}
Console.WriteLine();
// 也可以直接调用迭代器方法
foreach(var s in den.IteratorMethod_Enumable())
{
Console.Write(s + " "); // X Y
}
Console.WriteLine();

// ==========模式3:类没有GetEnumerator,不能foreach实例,直接调用迭代器==========
Demo_NoGetEnumerator dn = new Demo_NoGetEnumerator();
// foreach(var s in dn) // 编译报错!类没有GetEnumerator,实例不能直接foreach
foreach(var s in dn.IteratorMethod_Enumable())
{
Console.Write(s + " "); // M N
}
}
}
1
2
3
4
A B
X Y
X Y
M N

产生多个可枚举类型

  1. Spectrum没有实现GetEnumerator(),因此类实例本身不能直接放在foreach中使用(foreach(var x in spectrum)编译报错),类本身不是可枚举类型。
  2. 类中可以定义多个迭代器方法,每个迭代器返回IEnumerable<T>可枚举对象,对外提供不同的遍历序列(本例正序、逆序两套序列)。
  3. 使用方式:foreach直接调用迭代器方法spectrum.UVtoIR(),使用方法返回的可枚举对象完成遍历。
  4. yield return可以写在for循环内部,循环迭代时逐个产出元素,属于延迟执行;每次foreach触发,迭代器重新从头执行循环逻辑。
  5. 适用场景:一个类需要提供多种不同遍历方式,但是不需要把类本身作为可枚举对象对外暴露。
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" };

// 迭代器:返回IEnumerable<string>,正序:紫外线→红外线
public IEnumerable<string> UVtoIR()
{
for (int i=0; i < colors.Length; i++)
{
yield return colors[i];
}
}

// 迭代器:返回IEnumerable<string>,逆序:红外线→紫外线
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

将迭代器作为属性

  1. 迭代器可以写在属性的get访问器中,不只是只能写在方法里;属性返回类型为IEnumerator<T>,yield return写在get{ }内部。
  2. 本类实现了GetEnumerator(),类本身是可枚举类型,可以直接foreach (var x in spectrumInstance)。
    • GetEnumerator()利用三元运算符,根据对象内部布尔标记,动态选择返回两个迭代器属性之一。
    • 对象在构造的时候就确定遍历方向,同一个对象固定一套遍历顺序。
  3. 返回IEnumerator<T>迭代器(属性版本),产出枚举器;这里不能直接foreach(spectrum.UVtoIR),因为UVtoIR是枚举器IEnumerator<T>,不是可枚举IEnumerable<T>。
  4. 对比上一版:
    • 上例:多个IEnumerable< T>迭代器方法,类本身不可枚举,外部直接调用方法foreach。
    • 本例:多个IEnumerator< T>迭代器属性,类本身可枚举;由GetEnumerator()运行时选择具体使用哪一套枚举逻辑。
  5. 延迟执行依然生效:每次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;
}

// 实现GetEnumerator,类实例支持foreach,根据字段选择返回哪一个迭代器属性
public IEnumerator<string> GetEnumerator()
{
return _listFromUVtoIR ? UVtoIR : IRtoUV;
}

//迭代器实现为【属性】,get访问器内部使用yield return,返回枚举器IEnumerator<string>
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()
{
// true:正序遍历
Spectrum startUV = new Spectrum(true);
// false:逆序遍历
Spectrum startIR = new Spectrum(false);

// 直接foreach类实例,走类的GetEnumerator(),由构造时的标志决定遍历顺序
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

迭代器的实质

  1. 命名空间:使用迭代器/yield,必须引入using System.Collections.Generic;。
  2. Reset 方法:编译器自动生成的迭代器枚举器,Reset () 方法只是为了满足接口,实际调用直接抛NotSupportedException,不能用来重置迭代器状态;想要从头遍历,需要重新获取新枚举器。
  3. 编译器把 yield 迭代器编译为四状态状态机:
    • Before:初始,未调用过MoveNext()
    • Running:正在执行迭代器代码,执行yield return/yield break/方法结束就退出Running
    • Suspended:暂停,等待下一次MoveNext()恢复执行
    • After:迭代全部完成,无更多元素
  4. 状态流转:调用MoveNext() → 进入 Running;
    • 还有元素:切到Suspended暂停,保存现场;
    • 没有元素:切到After结束。
  5. foreach 每一轮本质就是调用MoveNext()驱动状态机在各个状态之间切换,实现暂停‑恢复执行,这就是yield延迟执行的底层原理。
    ![[csharp-16-yield-2.png]]