在迭代器(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个产品的价格兑换为美元