C# Yield 关键字
yield
用来在 Iterator 中提供返回值的关键字,yield return
和直接使用 return 不同的点在于它可以保持当前状态并在下次对 Iterator
调用 MoveNext
时继续执行,yield break
则是用来结束迭代器。
本文会首先通过用 Yield 改写代码的示例来说明 yield
的用法,然后会介绍 yield
更具体的工作原理。
#使用 Yield 改写代码的示例
#没有 Yield 的情况
我们通过一个非常简单的例子来说明 yield
的用法,假设我们有一个支付的列表,数量为 10000,对于 ID 数小于 10 的情况,我们打印 ID 并打印支付的名称,实现如下:
1 | internal abstract class Program |
运行结果为,可以看到正确的输出了 ID 从 0 到 9 的支付信息:
上述的代码并无错误,但如果进行 Debug 会发现其性能会有巨大浪费。因为在目前的实现中,我们是先生成了完整的 Payments 对象列表,再对其进行遍历,无论我们是否需要这些对象,都会生成这些对象,这样会导致性能的浪费。
如果有一种方式,可以在遍历 Payments 对象的时候只生成需要的对象,就可以避免性能的浪费。yield
语句就能满足这样的期望。
#使用 Yield 的情况
可以将上述的代码 使用 yield
关键字进行改写,只需要对生成 IEnumerable<Payment>
的方法进行修改即可,如下:
1 | // ..... |
此时运行结果与之前相同,但 Debug 的时候,可以看到运行到 ProcessPayment
函数的 in
关键字时,才会进入 GetPaymentsWithYield
函数并生成 Payment 对象,且后续每一次运行到 in
时都会再次进入 GetPaymentsWithYield
函数,即在每一次需要使用 IEnumerable<Payment>
中的元素时才会去真正的生成该对象,这就极大的节省了性能。
并不是只有 foreach
封装了对于 IEnumerator
的使用,也可以使用普通的 while
循环或者 for
循环来进行迭代,只不过需要使用 ElementAt
来获取元素。
1 | for (int i = 0; i != 10; i++) |
#Yield 是如何工作的
为了解 yield
是如何工作的,首先要明确 IEnumerable
和 IEnumerator
的工作原理。
#IEnumerable 和 IEnumerator 的构成
IEnumerable
和 IEnumerator
是用来实现迭代器的接口,IEnumerable
接口包含一个方法 GetEnumerator
,该方法返回一个实现了 IEnumerator
接口的对象,IEnumerator
接口包含了两个方法,MoveNext
和 Reset
,以及一个属性 Current
。
1 | public interface IEnumerable |
泛型版本的 IEnumerable<T>
和 IEnumerator<T>
和非泛型版本的接口类似,只是泛型版本的接口中的 Current
属性的类型是泛型类型 T
。
———— IEnumerable and IEnumerator
当使用 foreach
对 IEnumerable
对象进行迭代时,它实际上会对 IEumberable
对象调用 GetEnumerator
函数获取 IEnumerator
,并使用其中的 MoveNext
和 Current
判断是否仍然有下一个元素和获取当前元素。
即上述使用 foreach
的 ProcessPayment
函数会被编译成类似如下的代码:
1 | private static void ProcessPaymentUsingWhile() |
yield
的作用就是在迭代器中返回一个值或信号( yield break
表示迭代器结束),并保持迭代器的当前状态。当调用迭代器 MoveNext
时,其执行逻辑为:
- 如果是首次迭代,则执行语句直到遇到
yield return
,返回一个值,并保持当前状态,挂起迭代器。迭代器的调用者可以获取到这个值并对其进行处理 - 如果是后续迭代,则从上次挂起的地方继续执行,直到遇到下一个
yield return
或yield break
,返回一个值或信号,并保持当前状态,挂起迭代器。
这也就解释了为何在之前的 Debug 中,每一次 ProcessPayment
函数的 in
关键字处都会进入 GetPaymentsWithYield
函数:GetPaymentsWithYield
函数并没有真正的被完全执行完,而是在 yield
语句处返回了数据且保存了当前迭代器状态,等待一下次的进入。
根据如下代码,可以进一步验证 yield
的执行逻辑:
1 | public static void Output() |
当调用 Output
函数时,输出如下:
1 | Caller: about to iterate. |