yield 用来在 Iterator 中提供返回值的关键字,yield return 和直接使用 return 不同的点在于它可以保持当前状态并在下次对 Iterator 调用 MoveNext 时继续执行,yield break 则是用来结束迭代器。

本文会首先通过用 Yield 改写代码的示例来说明 yield 的用法,然后会介绍 yield 更具体的工作原理。

#使用 Yield 改写代码的示例

#没有 Yield 的情况

我们通过一个非常简单的例子来说明 yield 的用法,假设我们有一个支付的列表,数量为 10000,对于 ID 数小于 10 的情况,我们打印 ID 并打印支付的名称,实现如下:

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
internal abstract class Program
{
private static void Main() { ProcessPayment(); }

private static void ProcessPayment()
{
IEnumerable<Payment> payment = GetPayments(10000);
foreach (Payment p in payment)
{
if (p.id < 10)
Console.WriteLine($"Payment ID: {p.id}, Name: {p.name}");
else
break;
}
}

private static IEnumerable<Payment> GetPayments(int count)
{
var payments = new List<Payment>();

for (int i = 0; i != count; i++)
{
var p = new Payment();
p.id = i;
p.name = "Payment " + i;
payments.Add(p);
}

return payments;
}
}

运行结果为,可以看到正确的输出了 ID 从 0 到 9 的支付信息:
运行结果

上述的代码并无错误,但如果进行 Debug 会发现其性能会有巨大浪费。因为在目前的实现中,我们是先生成了完整的 Payments 对象列表,再对其进行遍历,无论我们是否需要这些对象,都会生成这些对象,这样会导致性能的浪费。
10000 个 Payment 对象

如果有一种方式,可以在遍历 Payments 对象的时候只生成需要的对象,就可以避免性能的浪费。yield 语句就能满足这样的期望。

#使用 Yield 的情况

可以将上述的代码 使用 yield 关键字进行改写,只需要对生成 IEnumerable<Payment> 的方法进行修改即可,如下:

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
// .....

private static void ProcessPayment()
{
// IEnumerable<Payment> payment = GetPayments(10000);
IEnumerable<Payment> payment = GetPaymentsWithYield(10000);
foreach (Payment p in payment)
{
if (p.id < 10)
Console.WriteLine($"Payment ID: {p.id}, Name: {p.name}");
else
break;
}
}

private static IEnumerable<Payment> GetPaymentsWithYield(int count)
{
for (int i = 0; i != count; i++)
{
var p = new Payment();
p.id = i;
p.name = "Payment " + i;
yield return p;
}
}

此时运行结果与之前相同,但 Debug 的时候,可以看到运行到 ProcessPayment 函数的 in 关键字时,才会进入 GetPaymentsWithYield 函数并生成 Payment 对象,且后续每一次运行到 in 时都会再次进入 GetPaymentsWithYield 函数,即在每一次需要使用 IEnumerable<Payment> 中的元素时才会去真正的生成该对象,这就极大的节省了性能。

仅在需要时生成对象

并不是只有 foreach 封装了对于 IEnumerator 的使用,也可以使用普通的 while 循环或者 for 循环来进行迭代,只不过需要使用 ElementAt 来获取元素。

1
2
3
4
5
for (int i = 0; i != 10; i++)
{
Payment p = payment.ElementAt(i);
Console.WriteLine($"Payment ID: {p.id}, Name: {p.name}");
}

#Yield 是如何工作的

为了解 yield 是如何工作的,首先要明确 IEnumerableIEnumerator 的工作原理。

#IEnumerable 和 IEnumerator 的构成

IEnumerableIEnumerator 是用来实现迭代器的接口,IEnumerable 接口包含一个方法 GetEnumerator,该方法返回一个实现了 IEnumerator 接口的对象,IEnumerator 接口包含了两个方法,MoveNextReset,以及一个属性 Current

1
2
3
4
5
6
7
8
9
10
11
public interface IEnumerable
{
IEnumerator GetEnumerator();
}

public interface IEnumerator
{
bool MoveNext();
void Reset();
object Current { get; }
}

泛型版本的 IEnumerable<T>IEnumerator<T> 和非泛型版本的接口类似,只是泛型版本的接口中的 Current 属性的类型是泛型类型 T

———— IEnumerable and IEnumerator

当使用 foreachIEnumerable 对象进行迭代时,它实际上会对 IEumberable 对象调用 GetEnumerator 函数获取 IEnumerator,并使用其中的 MoveNextCurrent 判断是否仍然有下一个元素和获取当前元素。

即上述使用 foreachProcessPayment 函数会被编译成类似如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
private static void ProcessPaymentUsingWhile()
{
IEnumerable<Payment> payment = GetPaymentsWithYield(10000);
using IEnumerator<Payment> paymentEnumerator = payment.GetEnumerator();
while (paymentEnumerator.MoveNext())
{
Payment p = paymentEnumerator.Current;
if (p.id < 10)
Console.WriteLine($"Payment ID: {p.id}, Name: {p.name}");
else
break;
}
}

yield 的作用就是在迭代器中返回一个值或信号( yield break 表示迭代器结束),并保持迭代器的当前状态。当调用迭代器 MoveNext 时,其执行逻辑为:

  • 如果是首次迭代,则执行语句直到遇到 yield return,返回一个值,并保持当前状态,挂起迭代器。迭代器的调用者可以获取到这个值并对其进行处理
  • 如果是后续迭代,则从上次挂起的地方继续执行,直到遇到下一个 yield returnyield break,返回一个值或信号,并保持当前状态,挂起迭代器。

这也就解释了为何在之前的 Debug 中,每一次 ProcessPayment 函数的 in 关键字处都会进入 GetPaymentsWithYield 函数:GetPaymentsWithYield 函数并没有真正的被完全执行完,而是在 yield 语句处返回了数据且保存了当前迭代器状态,等待一下次的进入。

根据如下代码,可以进一步验证 yield 的执行逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void Output()
{
var numbers = ProduceEvenNumbers(5);
Console.WriteLine("Caller: about to iterate.");
foreach (int i in numbers)
{
Console.WriteLine($"Caller: {i}");
}
}

private static IEnumerable<int> ProduceEvenNumbers(int upto)
{
Console.WriteLine("Iterator: start.");

for (int i = 0; i <= upto; i += 2)
{
Console.WriteLine($"Iterator: about to yield {i}");
yield return i;
Console.WriteLine($"Iterator: yielded {i}");
}

Console.WriteLine("Iterator: end.");
}

当调用 Output 函数时,输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
Caller: about to iterate.
Iterator: start.
Iterator: about to yield 0
Caller: 0
Iterator: yielded 0
Iterator: about to yield 2
Caller: 2
Iterator: yielded 2
Iterator: about to yield 4
Caller: 4
Iterator: yielded 4
Iterator: end

#Reference