C# 中yield 的使用详解
总目录
前言
当我们编写 C# 代码时,经常需要处理大量的数据集合。在传统的方式中,我们往往需要先将整个数据集合加载到内存中,然后再进行操作。但是如果数据集合非常大,这种方式就会导致内存占用过高,甚至可能导致程序崩溃。
C# 中的yield return机制可以帮助我们解决这个问题。通过使用yield return,我们可以将数据集合按需生成,而不是一次性生成整个数据集合。这样可以大大减少内存占用,并且提高程序的性能。
一、IEnumerable 和 IEnumerator
1. IEnumerable
IEnumerable 接口,是可枚举的所有非泛型集合的基接口
公开枚举数,该枚举数支持在非泛型集合上进行简单迭代。其泛型等效项是 System.Collections.Generic.IEnumerable<T>
接口
public interface IEnumerable
{
//返回可循环遍历的集合。
IEnumerator GetEnumerator();
}
2. IEnumerator
IEnumerator 接口,是所有非泛型枚举器的基接口
支持对非泛型集合的简单迭代,其泛型等效项是 System.Collections.Generic.IEnumerator<T>
接口。
//支持对非泛型集合进行简单迭代。
public interface IEnumerator
{
// 获取集合中枚举数当前位置的元素。
object? Current { get; }
// 将枚举数前进到集合的下一个元素。
// 返回结果: 如果枚举数成功推进到下一个元素,则为True;如果枚举数已经过集合的末尾,则为False。
bool MoveNext();
// 将枚举数设置为其初始位置,即在集合中的第一个元素之前。
void Reset();
}
3. 作用
IEnumerable 和 IEnumerator 一般用于实现自定义集合。一个容器Collection要支持foreach方式的遍历,必须实现IEnumerable接口或者必须以某种方式返回IEnumerator object来实现。
通俗讲:C#代码中可以使用Foreach 循环遍历一个List<T> 或者是数组,是因为这些对象实现了IEnumerable 接口,而我们一般自定义的class 如果不实现IEnumerable 接口,是不支持通过foreach循环遍历的,即是说如果我们需要自定义的集合对象能支持循环遍历,就需要实现IEnumerable 接口。
4. 示例
下面的代码示例演示了通过实现 IEnumerable 和 IEnumerator 接口来循环访问自定义集合的最佳做法。
// Simple business object.
public class Person
{
public Person(string fName, string lName)
{
this.firstName = fName;
this.lastName = lName;
}
public string firstName;
public string lastName;
}
// Person对象的集合。此类实现了IEnumerable,因此可以与ForEach语法一起使用。
public class People : IEnumerable
{
private Person[] _people;
public People(Person[] pArray)
{
_people = new Person[pArray.Length];
for (int i = 0; i < pArray.Length; i++)
{
_people[i] = pArray[i];
}
}
// GetEnumerator方法的实现
IEnumerator IEnumerable.GetEnumerator()
{
return (IEnumerator)GetEnumerator();
}
public PeopleEnum GetEnumerator()
{
return new PeopleEnum(_people);
}
}
// 实现IEnumerable时,还必须实现IEnumerator。
public class PeopleEnum : IEnumerator
{
public Person[] _people;
// Enumerators are positioned before the first element
// until the first MoveNext() call.
int position = -1;
public PeopleEnum(Person[] list)
{
_people = list;
}
public bool MoveNext()
{
position++;
return (position < _people.Length);
}
public void Reset()
{
position = -1;
}
object IEnumerator.Current
{
get
{
return Current;
}
}
public Person Current
{
get
{
try
{
return _people[position];
}
catch (IndexOutOfRangeException)
{
throw new InvalidOperationException();
}
}
}
}
调用:
static void Main()
{
Person[] peopleArray = new Person[3]
{
new Person("John", "Smith"),
new Person("Jim", "Johnson"),
new Person("Sue", "Rabon"),
};
People peopleList = new People(peopleArray);
foreach (Person p in peopleList)
Console.WriteLine(p.firstName + " " + p.lastName);
}
二、迭代器
1. 基本介绍
-
迭代器可用于逐步迭代集合,例如列表和数组。
-
通过 yield 返回的 IEnumerable<T> 类型,表示这是一个可以被遍历的数据集合。它之所以可以被遍历,是因为它实现了一个标准的 IEnumerable 接口。一般,我们把像上面这种包含 yield 语句并返回 IEnumerable<T> 类型的方法称为迭代器(Iterator)。【注意:包含 yield 语句的方法的返回类型也可以是 IEnumerator<T>,它比迭代器更低一个层级,迭代器是列举器的一种实现。】
-
迭代器方法或 get 访问器可对集合执行自定义迭代。 迭代器方法使用 yield return 语句返回元素,每次返回一个。 到达 yield return 语句时,会记住当前在代码中的位置。 下次调用迭代器函数时,将从该位置重新开始执行。(通俗讲就是使用 yield return 上下文关键字的方法就是迭代器方法)
-
通过 foreach 语句或 LINQ 查询从客户端代码中使用迭代器。
在以下示例中,foreach 循环的首次迭代导致 SomeNumbers 迭代器方法继续执行,直至到达第一个 yield return 语句。 此迭代返回的值为 3,并保留当前在迭代器方法中的位置。 在循环的下次迭代中,迭代器方法的执行将从其暂停的位置继续,直至到达 yield return 语句后才会停止。 此迭代返回的值为 5,并再次保留当前在迭代器方法中的位置。 到达迭代器方法的结尾时,循环便已完成。
static void Main()
{
foreach (int number in SomeNumbers())
{
Console.Write(number.ToString() + " ");
}
// Output: 3 5 8
Console.ReadKey();
}
public static System.Collections.IEnumerable SomeNumbers()
{
yield return 3;
yield return 5;
yield return 8;
}
迭代器方法或 get 访问器的返回类型可以是 IEnumerable、IEnumerable<T>、IEnumerator 或 IEnumerator<T>。
可以使用 yield break 语句来终止迭代。
2. 简单的迭代器
下例包含一个位于 for 循环内的 yield return 语句。 在 Main 中,foreach 语句体的每次迭代都会创建一个对迭代器函数的调用,并将继续到下一个 yield return 语句。
static void Main()
{
foreach (int number in EvenSequence(5, 18))
{
Console.Write(number.ToString() + " ");
}
// Output: 6 8 10 12 14 16 18
Console.ReadKey();
}
public static System.Collections.Generic.IEnumerable<int>
EvenSequence(int firstNumber, int lastNumber)
{
// Yield even numbers in the range.
for (int number = firstNumber; number <= lastNumber; number++)
{
if (number % 2 == 0)
{
yield return number;
}
}
}
3. 创建集合类
在以下示例中,DaysOfTheWeek 类实现 IEnumerable 接口,此操作需要 GetEnumerator 方法。 编译器隐式调用 GetEnumerator 方法,此方法返回 IEnumerator。
GetEnumerator 方法通过使用 yield return 语句每次返回 1 个字符串。
static void Main()
{
DaysOfTheWeek days = new DaysOfTheWeek();
foreach (string day in days)
{
Console.Write(day + " ");
}
// Output: Sun Mon Tue Wed Thu Fri Sat
Console.ReadKey();
}
public class DaysOfTheWeek : IEnumerable
{
private string[] days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
public IEnumerator GetEnumerator()
{
for (int index = 0; index < days.Length; index++)
{
// Yield each day of the week.
yield return days[index];
}
}
}
下例创建了一个包含动物集合的 Zoo 类。
引用类实例 (theZoo) 的 foreach 语句隐式调用 GetEnumerator 方法。 引用 Birds 和 Mammals 属性的 foreach 语句使用 AnimalsForType 命名迭代器方法。
static void Main()
{
Zoo theZoo = new Zoo();
theZoo.AddMammal("Whale");
theZoo.AddMammal("Rhinoceros");
theZoo.AddBird("Penguin");
theZoo.AddBird("Warbler");
foreach (string name in theZoo)
{
Console.Write(name + " ");
}
Console.WriteLine();
// Output: Whale Rhinoceros Penguin Warbler
foreach (string name in theZoo.Birds)
{
Console.Write(name + " ");
}
Console.WriteLine();
// Output: Penguin Warbler
foreach (string name in theZoo.Mammals)
{
Console.Write(name + " ");
}
Console.WriteLine();
// Output: Whale Rhinoceros
Console.ReadKey();
}
public class Zoo : IEnumerable
{
// Private members.
private List<Animal> animals = new List<Animal>();
// Public methods.
public void AddMammal(string name)
{
animals.Add(new Animal { Name = name, Type = Animal.TypeEnum.Mammal });
}
public void AddBird(string name)
{
animals.Add(new Animal { Name = name, Type = Animal.TypeEnum.Bird });
}
public IEnumerator GetEnumerator()
{
foreach (Animal theAnimal in animals)
{
yield return theAnimal.Name;
}
}
// Public members.
public IEnumerable Mammals
{
get { return AnimalsForType(Animal.TypeEnum.Mammal); }
}
public IEnumerable Birds
{
get { return AnimalsForType(Animal.TypeEnum.Bird); }
}
// Private methods.
private IEnumerable AnimalsForType(Animal.TypeEnum type)
{
foreach (Animal theAnimal in animals)
{
if (theAnimal.Type == type)
{
yield return theAnimal.Name;
}
}
}
// Private class.
private class Animal
{
public enum TypeEnum { Bird, Mammal }
public string Name { get; set; }
public TypeEnum Type { get; set; }
}
}
4. 对泛型列表使用迭代器
在以下示例中,Stack 泛型类实现 IEnumerable 泛型接口。 Push 方法将值分配给类型为 T 的数组。 GetEnumerator 方法通过使用 yield return 语句返回数组值。
除了泛型 GetEnumerator 方法,还必须实现非泛型 GetEnumerator 方法。 这是因为从 IEnumerable 继承了 IEnumerable。 非泛型实现遵从泛型实现的规则。
本示例使用命名迭代器来支持通过各种方法循环访问同一数据集合。 这些命名迭代器为 TopToBottom 和 BottomToTop 属性,以及 TopN 方法。
BottomToTop 属性在 get 访问器中使用迭代器。
static void Main()
{
Stack<int> theStack = new Stack<int>();
// Add items to the stack.
for (int number = 0; number <= 9; number++)
{
theStack.Push(number);
}
// Retrieve items from the stack.
// foreach is allowed because theStack implements IEnumerable<int>.
foreach (int number in theStack)
{
Console.Write("{0} ", number);
}
Console.WriteLine();
// Output: 9 8 7 6 5 4 3 2 1 0
// foreach is allowed, because theStack.TopToBottom returns IEnumerable(Of Integer).
foreach (int number in theStack.TopToBottom)
{
Console.Write("{0} ", number);
}
Console.WriteLine();
// Output: 9 8 7 6 5 4 3 2 1 0
foreach (int number in theStack.BottomToTop)
{
Console.Write("{0} ", number);
}
Console.WriteLine();
// Output: 0 1 2 3 4 5 6 7 8 9
foreach (int number in theStack.TopN(7))
{
Console.Write("{0} ", number);
}
Console.WriteLine();
// Output: 9 8 7 6 5 4 3
Console.ReadKey();
}
public class Stack<T> : IEnumerable<T>
{
private T[] values = new T[100];
private int top = 0;
public void Push(T t)
{
values[top] = t;
top++;
}
public T Pop()
{
top--;
return values[top];
}
// This method implements the GetEnumerator method. It allows
// an instance of the class to be used in a foreach statement.
public IEnumerator<T> GetEnumerator()
{
for (int index = top - 1; index >= 0; index--)
{
yield return values[index];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public IEnumerable<T> TopToBottom
{
get { return this; }
}
public IEnumerable<T> BottomToTop
{
get
{
for (int index = 0; index <= top - 1; index++)
{
yield return values[index];
}
}
}
public IEnumerable<T> TopN(int itemsFromTop)
{
// Return less than itemsFromTop if necessary.
int startIndex = itemsFromTop >= top ? 0 : top - itemsFromTop;
for (int index = top - 1; index >= startIndex; index--)
{
yield return values[index];
}
}
}
5. 使用注意事项
迭代器可用作一种方法,或一个 get 访问器。 不能在事件、实例构造函数、静态构造函数或静态终结器中使用迭代器。
必须存在从 yield return 语句中的表达式类型到迭代器返回的 IEnumerable 类型参数的隐式转换。
在 C# 中,迭代器方法不能有任何 in、ref 或 out 参数。
在 C# 中,yield 不是保留字,只有在 return 或 break 关键字之前使用时才有特殊含义。
三、迭代器二
1. 基本介绍
迭代器是遍历容器的对象,尤其是列表。 迭代器可用于:
- 对集合中的每个项执行操作。
- 枚举自定义集合。
- 扩展 LINQ 或其他库。
- 创建数据管道,以便数据通过迭代器方法在管道中有效流动。
- C# 语言提供用于生成和使用序列的功能。 可以同步或异步生成和使用这些序列。
2. 使用 foreach 执行循环访问
枚举集合非常简单:使用 foreach 关键字枚举集合,从而为集合中的每个元素执行一次嵌入语句:
foreach (var item in collection)
{
Console.WriteLine(item?.ToString());
}
就这样。 若要循环访问集合中的所有内容,只需使用 foreach 语句。 但 foreach 语句并非完美无缺。 它依赖于 .NET Core 库中定义的 2 个泛型接口,才能生成循环访问集合所需的代码:IEnumerable<T> 和 IEnumerator<T>。 下文对此机制进行了更详细说明。
这 2 种接口还具备相应的非泛型接口:IEnumerable 和 IEnumerator。 泛型版本是新式代码的首要选项。
异步生成序列时,可以使用 await foreach 语句异步使用此序列:
await foreach (var item in asyncSequence)
{
Console.WriteLine(item?.ToString());
}
如果序列是 System.Collections.Generic.IEnumerable<T>,则使用 foreach。 如果序列是 System.Collections.Generic.IAsyncEnumerable<T>,则使用 await foreach。 在后一种情况下,序列是异步生成的。
3. 使用迭代器方法的枚举源
借助 C# 语言的另一个强大功能,能够生成创建枚举源的方法。 这些方法称为“迭代器方法”。 迭代器方法用于定义请求时如何在序列中生成对象。 使用 yield return 上下文关键字定义迭代器方法。
可编写此方法以生成从 0 到 9 的整数序列:
public IEnumerable<int> GetSingleDigitNumbers()
{
yield return 0;
yield return 1;
yield return 2;
yield return 3;
yield return 4;
yield return 5;
yield return 6;
yield return 7;
yield return 8;
yield return 9;
}
上方的代码显示了不同的 yield return 语句,以强调可在迭代器方法中使用多个离散 yield return 语句这一事实。 可以使用其他语言构造来简化迭代器方法的代码,这也是一贯的做法。 以下方法定义可生成完全相同的数字序列:
public IEnumerable<int> GetSingleDigitNumbersLoop()
{
int index = 0;
while (index < 10)
yield return index++;
}
不必从中选择一个。 可根据需要提供尽可能多的 yield return 语句来满足方法需求:
public IEnumerable<int> GetSetsOfNumbers()
{
int index = 0;
while (index < 10)
yield return index++;
yield return 50;
index = 100;
while (index < 110)
yield return index++;
}
上述所有示例都有一个异步对应项。 在每种情况下,将 IEnumerable 的返回类型替换为 IAsyncEnumerable。 例如,前面的示例将具有以下异步版本:
public async IAsyncEnumerable<int> GetSetsOfNumbersAsync()
{
int index = 0;
while (index < 10)
yield return index++;
await Task.Delay(500);
yield return 50;
await Task.Delay(500);
index = 100;
while (index < 110)
yield return index++;
}
这是同步和异步迭代器的语法。 我们来看一个真实示例。 假设你正在处理一个 IoT 项目,设备传感器生成了大量数据流。 为了获知数据,需要编写一个对每第 N 个数据元素进行采样的方法。 通过以下小迭代器方法可实现此目的:
public static IEnumerable<T> Sample<T>(this IEnumerable<T> sourceSequence, int interval)
{
int index = 0;
foreach (T item in sourceSequence)
{
if (index++ % interval == 0)
yield return item;
}
}
如果从 IoT 设备读取生成异步序列,则修改方法,如以下方法所示:
public static async IAsyncEnumerable<T> Sample<T>(this IAsyncEnumerable<T> sourceSequence, int interval)
{
int index = 0;
await foreach (T item in sourceSequence)
{
if (index++ % interval == 0)
yield return item;
}
}
迭代器方法有一个重要限制:在同一方法中不能同时使用 return 语句和 yield return 语句。 以下代码无法编译:
public IEnumerable<int> GetSingleDigitNumbers()
{
int index = 0;
while (index < 10)
yield return index++;
yield return 50;
// generates a compile time error:
var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
return items;
}
此限制通常不是问题。 可以选择在整个方法中使用 yield return,或选择将原始方法分成多个方法,一些使用 return,另一些使用 yield return。
可略微修改一下最后一个方法,使其可在任何位置使用 yield return:
public IEnumerable<int> GetFirstDecile()
{
int index = 0;
while (index < 10)
yield return index++;
yield return 50;
var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
foreach (var item in items)
yield return item;
}
有时,正确的做法是将迭代器方法拆分成 2 个不同的方法。 一个使用 return,另一个使用 yield return。 考虑这样一种情况:需要基于布尔参数返回一个空集合,或者返回前 5 个奇数。 可编写类似以下 2 种方法的方法:
public IEnumerable<int> GetSingleDigitOddNumbers(bool getCollection)
{
if (getCollection == false)
return new int[0];
else
return IteratorMethod();
}
private IEnumerable<int> IteratorMethod()
{
int index = 0;
while (index < 10)
{
if (index % 2 == 1)
yield return index;
index++;
}
}
看看上面的方法。 第 1 个方法使用标准 return 语句返回空集合,或返回第 2 个方法创建的迭代器。 第 2 个方法使用 yield return 语句创建请求的序列
四、yield
1. 基本介绍
yield 语句 - 提供下一个元素
在迭代器中使用 yield 语句提供下一个值或表示迭代结束。
2. yield return 和 yield break
yield 语句有以下两种形式:
- yield return:在迭代中提供下一个值,如以下示例所示:
foreach (int i in ProduceEvenNumbers(9))
{
Console.Write(i);
Console.Write(" ");
}
// Output: 0 2 4 6 8
IEnumerable<int> ProduceEvenNumbers(int upto)
{
for (int i = 0; i <= upto; i += 2)
{
yield return i;
}
}
- yield break:显式示迭代结束,如以下示例所示:
Console.WriteLine(string.Join(" ", TakeWhilePositive(new int[] {2, 3, 4, 5, -1, 3, 4})));
// Output: 2 3 4 5
Console.WriteLine(string.Join(" ", TakeWhilePositive(new int[] {9, 8, 7})));
// Output: 9 8 7
IEnumerable<int> TakeWhilePositive(IEnumerable<int> numbers)
{
foreach (int n in numbers)
{
if (n > 0)
{
yield return n;
}
else
{
yield break;
}
}
}
当控件到达迭代器的末尾时,迭代也结束。
3. yield 的使用
在前面的示例中,迭代器的返回类型为 IEnumerable<T>(在非泛型情况下,使用 IEnumerable 作为迭代器的返回类型)。 还可以使用 IAsyncEnumerable<T> 作为迭代器的返回类型。 这使得迭代器异步。 使用 await foreach 语句对迭代器的结果进行迭代,如以下示例所示:
await foreach (int n in GenerateNumbersAsync(5))
{
Console.Write(n);
Console.Write(" ");
}
// Output: 0 2 4 6 8
async IAsyncEnumerable<int> GenerateNumbersAsync(int count)
{
for (int i = 0; i < count; i++)
{
yield return await ProduceNumberAsync(i);
}
}
async Task<int> ProduceNumberAsync(int seed)
{
await Task.Delay(1000);
return 2 * seed;
}
迭代器的返回类型可以是 IEnumerator<T> 或 IEnumerator。 在以下方案中实现 GetEnumerator 方法时,请使用这些返回类型:
-
设计实现 IEnumerable<T> 或 IEnumerable 接口的类型。
-
添加实例或扩展 GetEnumerator 方法来使用 foreach 语句对类型的实例启用迭代,如以下示例所示:
public static void Example()
{
var point = new Point(1, 2, 3);
foreach (int coordinate in point)
{
Console.Write(coordinate);
Console.Write(" ");
}
// Output: 1 2 3
}
public readonly record struct Point(int X, int Y, int Z)
{
public IEnumerator<int> GetEnumerator()
{
yield return X;
yield return Y;
yield return Z;
}
}
不能在下列情况中使用 yield 语句:
- 带有 in、ref 或 out 参数的方法
- Lambda 表达式和匿名方法
- 不安全块。 在 C# 13 之前,yield 在具有 unsafe 块的任何方法中都无效。 从 C# 13 开始,可以在
- 包含 unsafe 块的方法中使用 yield,但不能在 unsafe 块中使用。
4. 迭代器的执行
迭代器的调用不会立即执行,如以下示例所示:
var numbers = ProduceEvenNumbers(5);
Console.WriteLine("Caller: about to iterate.");
foreach (int i in numbers)
{
Console.WriteLine($"Caller: {i}");
}
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:
// 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.
如前面的示例所示,当开始对迭代器的结果进行迭代时,迭代器会一直执行,直到到达第一个 yield return 语句为止。 然后,迭代器的执行会暂停,调用方会获得第一个迭代值并处理该值。 在后续的每次迭代中,迭代器的执行都会在导致上一次挂起的 yield return 语句之后恢复,并继续执行,直到到达下一个 yield return 语句为止。 当控件到达迭代器或 yield break 语句的末尾时,迭代完成。
五、其他
yield return将数据集合按需生成,而不是一次性生成整个数据集合。接下来通过一个简单的示例,我们看一下它的工作方式是什么样的,以便加深对它的理解
foreach (var num in GetInts())
{
Console.WriteLine("外部遍历了:{0}", num);
}
IEnumerable<int> GetInts()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("内部遍历了:{0}", i);
yield return i;
}
}
首先,在GetInts方法中,我们使用yield return关键字来定义一个迭代器。这个迭代器可以按需生成整数序列。在每次循环时,使用yield return返回当前的整数。通过1foreach循环来遍历 GetInts方法返回的整数序列。在迭代时GetInts方法会被执行,但是不会将整个序列加载到内存中。而是在需要时,按需生成序列中的每个元素。在每次迭代时,会输出当前迭代的整数对应的信息。所以输出的结果为
内部遍历了:0
外部遍历了:0
内部遍历了:1
外部遍历了:1
内部遍历了:2
外部遍历了:2
内部遍历了:3
外部遍历了:3
内部遍历了:4
外部遍历了:4
可以看到,整数序列是按需生成的,并且在每次生成时都会输出相应的信息。这种方式可以大大减少内存占用,并且提高程序的性能。当然从c# 8开始异步迭代的方式同样支持
await foreach (var num in GetIntsAsync())
{
Console.WriteLine("外部遍历了:{0}", num);
}
async IAsyncEnumerable<int> GetIntsAsync()
{
for (int i = 0; i < 5; i++)
{
await Task.Yield();
Console.WriteLine("内部遍历了:{0}", i);
yield return i;
}
}
和上面不同的是,如果需要用异步的方式,我们需要返回IAsyncEnumerable类型,这种方式的执行结果和上面同步的方式执行的结果是一致的,我们就不做展示了。
结语
以上就是本文的内容,希望以上内容可以帮助到您,如文中有不对之处,还请批评指正。
参考资料:
IEnumerable 接口
C#概念 - 迭代器
C#编程指南 - 迭代器
yield 语句 - 提供下一个元素
IEnumerable和IEnumerator 详解
C#内建接口:IEnumerable
[C#.NET 拾遗补漏] 理解 yield 关键字
C# 中的yield return机制和原理
深入理解C#中的yield关键字:提升迭代性能与效率
由C# yield return引发的思考
[C#.NET 拾遗补漏] 理解 yield 关键字