在迭代器(iterator)方法中,遇到yield return语句后就会返回,但是当前执行的位置会保留。当迭代器(iterator)再次被调用时会从上次的位置继续执行。


迭代器示例

IEnumerable<int> GetNumbers()
{
    yield return 1;
    yield return 2;
    yield return 3;
}

foreach (var item in GetNumbers())
{
    Console.WriteLine(item);
}

在visual studio使用逐语句调试执行,可以观察到程序在 foreach 循环和 GetNumber() 之间来回切换执行。当程序遇到 yield return 之后,便切换到 foreach 逻辑中并且返回 yield return 后面的数值。在 foreach 中执行完一次打印后程序又回到 GetNumbers() 中,并且从上次结束的位置开始执行。

传统循环 vs 迭代器

首先使用传统的循环来实现一个遍历的效果:

IEnumerable<int> GenerateWithoutYield() 
{ 
    var i = 0; 
    var list = new List<int>(); 
    while (i<5) 
        list.Add(++i); 
    return list; 
} 
foreach(var number in GenerateWithoutYield()) 
    Console.WriteLine(number);

代码执行步骤:
1. GenerateWithoutYield 被调用
2. 整个方法执行完毕并且返回构建好的数字列表
3. foreach 遍历列表中的所有值然后输出


使用迭代器实现:
IEnumerable<int> GenerateWithYield() 
{ 
    var i = 0; 
    while (i<5) 
        yield return ++i; 
} 
foreach(var number in GenerateWithYield()) 
    Console.WriteLine(number);

代码执行步骤:
1. GenerateWithYield() 被调用
2. 方法返回 IEnumerable<int> ,这里返回的并不是一个list,而是一个类似状态机运行机制的迭代器。以上代码中的 GenerateWithYield() 会返回5个数字,但并不是一次执行完毕。而是每次请求的时候逐一返回。
3. foreach 循环调用 GenerateWithYield() 知道所有数字输出完毕

无限循环

以上代码并不能展示迭代器的用处,下面用一个无限循环的例子来演示迭代器的优点

无限循环输出数字:

// 迭代器方法
IEnumerable<int> GenerateWithYield() 
{ 
    var i = 0; 
    while (true) 
    yield return ++i; 
} 

// 传统方法
IEnumerable<int> GenerateWithoutYield() 
{ 
    var i = 0; 
    var list = new List<int>(); 
    while (true) 
        list.Add(++i); 
    return list; 
}

// 打印数字
foreach(var number in GenerateWithoutYield())) 
    Console.WriteLine(number); 

foreach(var number in GenerateWithYield()) 
    Console.WriteLine(number);

结果很明显,如果使用传统的方法遍历数字程序会陷入死循环,而使用迭代器方法的话就会每次打印一个数字,逐步递增。

实际用途

自定义迭代

假如有一个数字列表,想要找出其中大于某一特定数值的数字集合。
如果使用传统的方法:

IEnumerable<int> GetNumbersGreaterThan3(List<int> numbers) 
{ 
    var theNumbers = new List<int>(); 
    foreach(var nr in numbers) 
    { 
        if(nr > 3) 
            theNumbers.Add(nr); 
    } 
    return theNumbers; 
} 
    foreach(var nr in GetNumbersGreaterThan3(new List<int> {1,2,3,4,5}) 
        Console.WriteLine(nr);

这种方法的确能够达到目的,但是需要创建一个额外的列表来保存结果。过程可用图表示为:
在这里插入图片描述

如果使用yield return :

IEnumerable<int> GetNumbersGreaterThan3(List<int> numbers) 
{ 
    foreach(var nr in numbers) 
    { 
        if(nr > 3) 
            yield return nr; 
    } 
} 
foreach(var nr in GetNumbersGreaterThan3(new List<int> {1,2,3,4,5}) 
    Console.WriteLine(nr);

使用yield return 的话过程图:
在这里插入图片描述

这样只需要迭代列表一次并且不需要创建额外的空间来保存结果。

状态控制

因为迭代器能够被暂停执行 或者在需要的时候 重新执行 ,所以该特性可以用来处理与状态有关的方法。

IEnumerable<int> Totals(List<int> numbers) 
{ 
    var total = 0; 
    foreach(var number in numbers) 
    { 
        total += number; 
        yield return total; 
    } 
} 
foreach(var total in Totals(new List<int> {1,2,3,4,5}) 
    Console.WriteLine(total);

// Output: 1    3    6    10    15

由于yield return 的暂停的重新运行特性,变量total能够输出每次迭代后的中间状态值。

延迟执行

以上的 yield return 代码都有一个特性,就是只有当需要的时候迭代器才会被调用。所以通过使用 yield return 能够让代码做到"延迟执行"(Deferred execution)

C# 的LINQ部分就用到了这一特性。示例:

var dollarPrices = FetchProducts().Take(10)
                                  .Select(p => p.CalculatePrice())
                                  .OrderBy(price => price)
                                  .Take(5)
                                  .Select(price => ConvertTo$(price));

假设有1000个产品,如果使用传统方法,那么需要:

  • 获取所有1000个产品
  • 计算所有产品的价格
  • 对价格进行排序
  • 将价格兑换为美元
  • 取出价格前5的产品

如果使用延迟执行的方法:

  • 取出10个产品
  • 计算取出产品的价格
  • 对价格进行排序
  • 将5个产品的价格兑换为美元

Reference

  1. https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/yield
  2. https://www.c-sharpcorner.com/UploadFile/5ef30d/understanding-yield-return-in-C-Sharp/
  3. https://www.kenneth-truyers.net/2016/05/12/yield-return-in-c/