迭代器可用于单步执行列表和数组等集合。
迭代器方法或 get
访问器对集合执行自定义迭代。 迭代器方法使用 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>或IEnumeratorIEnumerator<T>。
可以使用 yield break
语句结束迭代。
简单迭代器
以下示例包含一个位于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;
}
}
}
创建集合类
在下面的示例中,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
包含动物集合的类。
foreach
引用类实例 (theZoo
) 的语句隐式调用GetEnumerator
该方法。 引用 foreach
和 Birds
属性的 Mammals
语句使用 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; }
}
}
将迭代器与泛型列表配合使用
在以下示例中, Stack<T> 泛型类实现 IEnumerable<T> 泛型接口。 该方法 Push 将值分配给类型的 T
数组。 该方法 GetEnumerator 使用 yield return
语句返回数组值。
除了泛型 GetEnumerator 方法,还必须实现非泛型 GetEnumerator 方法。 这是因为 IEnumerable<T> 继承自 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];
}
}
}
语法信息
迭代器可用作一种方法,或一个 get
访问器。 迭代器不能在事件、实例构造函数、静态构造函数或静态终结器中发生。
从语句中的 yield return
表达式类型到迭代器返回的类型参数 IEnumerable<T>
,必须存在隐式转换。
在 C# 中,迭代器方法不能有任何in
或ref
out
参数。
在 C# 中,yield
不是保留字,仅在紧接在 return
或 break
关键字之前使用时才具有特殊含义。
技术实现
尽管将迭代器编写为方法,但编译器会将它转换为实际上为状态机的嵌套类。 只要客户端代码中的 foreach
循环继续,此类就会跟踪迭代器的位置。
若要查看编译器的作用,可以使用 Ildasm.exe 工具查看为迭代器方法生成的公共中间语言代码。
为 类 或 结构创建迭代器时,无需实现整个 IEnumerator 接口。 当编译器检测到迭代器时,它会自动生成Current
或MoveNext
接口中的Dispose
、IEnumerator和IEnumerator<T>方法。
在 foreach
循环(或对 IEnumerator.MoveNext
的直接调用)的每次后续迭代中,下一个迭代器代码体都会在上一个 yield return
语句之后恢复。 然后,它会继续执行下一个 yield return
语句,直到迭代器正文结束,或者遇到 yield break
语句为止。
迭代器不支持该方法 IEnumerator.Reset 。 若要从一开始就重申,必须获取新的迭代器。 在迭代器方法返回的迭代器上调用 Reset 会引发 NotSupportedException。
有关详细信息,请参阅 C# 语言规范。
使用迭代器
当需要使用复杂代码填充列表序列时,迭代器使你能够保持循环的 foreach
简单性。 若要执行以下操作,这会很有帮助:
在第一次
foreach
循环迭代后修改列表序列。避免在
foreach
循环的第一次迭代之前完全加载大型列表。 一个示例是用于加载一批表格行的分页提取。 另一个示例是在 EnumerateFiles .NET 中实现迭代器的方法。在迭代器中封装列表的构建过程。 在迭代器方法中,可以生成列表,然后在循环中生成每个结果。