CS4L22——迭代器

本章代码关键字

1
2
3
4
5
IEnumerable            // 可迭代接口,需要实现返回迭代器IEnumerator的方法
IEnumerator // 迭代器接口,内部维护一个光标来迭代获取其中的元素
IEnumerable<T> // 可迭代泛型接口,需要实现返回迭代器IEnumerator<T>的方法,可以限制foreach迭代的变量类型
IEnumerator<T> // 迭代器泛型接口,内部维护一个光标来迭代获取其中的元素,迭代的类型被泛型约束
yield return // 返回值为IEnumerator的方法可以使用的语法糖,相当于返回一个值后挂起函数,再次执行函数时会从上次yield return的地方向后执行

迭代器

迭代器(iterator​)有时又称光标(cursor​),是程序设计的软件设计模式,
迭代器模式提供一个方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的标识

从表现结果上看,迭代器是可以在容器对象(例如链表或者数组)上遍历访问的接口,设计人员无需关心容器对象的内存分配的实现细节

可以用 foreach​ 遍历的类,都是实现了迭代器的接口

标准迭代器的实现方法

  • 关键接口:IEnumerator​,IEnumerable
  • 命名空间:using System.Collections;

迭代器的一个最常用的用途就是 foreach​ 遍历,首先拆解 foreach​ 遍历执行的内容

foreach 的本质

拆解 foreach​ 中具体每一步做的事情:

  1. 首先会调用 in​ 后面这个对象的 GetEnumerator()​ 方法,来获取 IEnumerator​ 对象

  2. 执行得到的这个 IEnumerator​ 对象中的 MoveNext()​ 方法

    其中,前两个步骤只会执行一次!接下来就是反复循环三四步,直到第三步的 MoveNext()​ 返回 false

  3. 只要 MoveNext()​ 方法的返回值是 true​,就会去从 IEnumerator​ 的 Current​ 属性取值,然后赋值给 in​ 前面的变量,执行 foreach​ 语句块

  4. 执行 foreach​ 语句块执行完毕,回到第三步,直到 MoveNext()​ 方法返回 false​,foreach​ 结束

因此,如果要使自定义类可以使用 foreach​ 遍历,
则必须要实现一个 GetEnumerator​ 方法,也就是 IEnumerable​ 内声明的方法,返回的 IEnumerator​ 对象就是被迭代的主体

而被迭代的,实现 IEnumerator​ 的对象,需要在内部存储一个光标(cursor​),类似于当前迭代到的对象,索引之类,
然后,在 MoveNext()​ 方法中,需要将这个光标向后移动,让光标指向下一个对象,根据对象存在与否返回 bool​ 值,
如果光标移动后可以指向一个对象,就返回 true​,告诉外部可以通过 Current​ 取值,如果光标越界或溢出,就返回 false​,告诉外部迭代结束,
Reset()​ 方法则需要将光标移动到最开始的位置上,同时,此方法一般是在内部调用,因此,可以在 GetEnumerator()​ 方法处调用

一个类可以通过同时继承 IEnumerator​ 和 IEnumerable​ 并实现其中的方法,也可以只继承 IEnumerable​,然后返回另一个 IEnumerator​ 对象

假设自定义类同时继承 IEnumerator​ 和 IEnumerable​ 并实现其中的方法,让 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
using System.Collections;

class CustomList : IEnumerable, IEnumerator
{
private int[] list;
private int position = -1;

public CustomList()
{
list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 };
}

// IEnumerable需要实现的内容
public IEnumerator GetEnumerator()
{
return this;
}

// IEnumerator需要实现的内容
public object Current => list[position];
// 移动光标,迭代到下一次
public bool MoveNext()
{
position++; // 移动光标
return position < list.Length; // 检测光标是否溢出越界,溢出越界就证明迭代结束
}
// 重置光标,一般在IEnumerable.GetEnumerator()内调用
public void Reset()
{
position = -1;
}
}

这样,foreach​ 就可以正常迭代了

1
2
3
4
5
CustomList list = new CustomList();
foreach (int item in list)
{
Console.WriteLine(item);
}

输出:

1
2
3
4
5
6
7
8
1
2
3
4
5
6
7
8

用 yield return 语法糖实现迭代器

yield return​ 是 C# 提供给我们的语法糖,它可以用于返回值为 IEnumerator​ 的函数内
所谓语法糖,也称糖衣语法,主要作用就是将复杂逻辑简单化,可以增加程序的可读性,从而减少程序代码出错的机会

yield​ 关键字是配合迭代器使用,可以理解为暂时返回,保留当前的状态,
下次调用从 yield return​ 的位置继续,原理其实和上面的标准迭代器一样,但是 C# 帮我们自动写好了代码

  • 关键接口:IEnumerable
  • 命名空间:using System.Collections;

让想要通过 foreach​ 遍历的自定义类实现接口中的方法 GetEnumerator​ 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System.Collections;

class YieldTest : IEnumerable
{
private int[] list;

public YieldTest()
{
list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 };
}

public IEnumerator GetEnumerator()
{
for (int i = 0; i < list.Length; i++)
{
yield return list[i]; // 走到这里先返回一个值,下次调用的时候在接着这里继续向下执行
}
}
}

yield​ 关键字背后的原理和之前实现的标准迭代器一样,只是 C# 编译器后续会根据这个关键字自动生成那些标准迭代器语句

1
2
3
4
5
YieldTest list = new YieldTest();
foreach (int item in list)
{
Console.WriteLine(item);
}

输出:

1
2
3
4
5
6
7
8
1
2
3
4
5
6
7
8

用 yield return 语法糖为泛型类实现迭代器

泛型类也可以直接使用 IEnumerable​ 以支持迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class YieldTest<T> : IEnumerable
{
private T[] array;

public YieldTest(T[] array)
{
this.array = array;
}

public IEnumerator GetEnumerator()
{
for (int i = 0; i < array.Length; i++)
{
yield return array[i]; // 走到这里先返回一个值,下次调用的时候在接着这里继续向下执行
}
}
}
1
2
3
4
5
YieldTest<string> list = new YieldTest<string>(new string[] { "aaa", "bbb", "ccc", "ddd", "eee" });
foreach (string item in list)
{
Console.WriteLine(item);
}

输出:

1
2
3
4
5
aaa
bbb
ccc
ddd
eee

IEnumerable​ 也有泛型接口:IEnumerable<T>​,它可以限制迭代器迭代的类型,同时也可以限制 foreach​ 循环时的变量类型,避免类型出错,
由于 IEnumerable<T>​ 继承了 IEnumerable​,因此实现 IEnumerator<T> GetEnumerator()​ 方法的同时,
还需要实现 IEnumerator GetEnumerator()​,但因为和泛型函数同名了,因此需要显式实现接口: IEnumerator IEnumerable.GetEnumerator()

这样,除非将 IEnumerable<T>​ 对象得到的 IEnumerator<T>​ 对象转换为 IEnumerator​ 对象,否则迭代器迭代的类型都是由 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
class YieldTest<T> : IEnumerable<T>
{
private T[] array;

public YieldTest(T[] array)
{
this.array = array;
}

public IEnumerator<T> GetEnumerator()
{
for (int i = 0; i < array.Length; i++)
{
// yield return 1; // error: 无法将类型“int”隐式转换为“T”
yield return array[i]; // 走到这里先返回一个值,下次调用的时候在接着这里继续向下执行
}
}

// 这个函数也可以直接 return GetEnumerator()(返回值为IEnumerator<T>的),因为IEnumerator<T>是继承于IEnumerator的
IEnumerator IEnumerable.GetEnumerator()
{
for (int i = 0; i < array.Length; i++)
{
yield return 1; // 这里可以返回任意类型的值,因为IEnumerator不限制迭代器迭代的类型
yield return array[i]; // 走到这里先返回一个值,下次调用的时候在接着这里继续向下执行
}
}
}
1
2
3
4
5
6
YieldTest<string> list = new YieldTest<string>(new string[] { "aaa", "bbb", "ccc", "ddd", "eee" });
foreach (string item in list)
{
Console.WriteLine(item);
}
// foreach (int item in list) {} // error: 无法将类型“int”隐式转换为“T”

输出:

1
2
3
4
5
aaa
bbb
ccc
ddd
eee