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 8Console.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 18Console.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 SatConsole.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 Warblerforeach (string name in theZoo.Birds){Console.Write(name + " ");}Console.WriteLine();// Output: Penguin Warblerforeach (string name in theZoo.Mammals){Console.Write(name + " ");}Console.WriteLine();// Output: Whale RhinocerosConsole.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 0foreach (int number in theStack.BottomToTop){Console.Write("{0} ", number);}Console.WriteLine();// Output: 0 1 2 3 4 5 6 7 8 9foreach (int number in theStack.TopN(7)){Console.Write("{0} ", number);}Console.WriteLine();// Output: 9 8 7 6 5 4 3Console.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];elsereturn 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 8IEnumerable<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 5Console.WriteLine(string.Join(" ", TakeWhilePositive(new int[] {9, 8, 7})));
// Output: 9 8 7IEnumerable<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 8async 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 关键字