【译】C#迭代器
原文链接:传送门。
一个迭代器而用来步进一个集合,比如列表或者数组。
一个迭代器方法或者get访问器将会在一个集合上执行一个自定义的迭代器。一个迭代器方法使用yield return 语句来每次返回集合中的一个元素。当程序的执行流达到一个yeild 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, or IEnumerator<T>。
你也可以使用yield break语句来结束迭代。
简单迭代器
如下的示例包含一个在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; } } }
创建一个集合类
在如下的示例中,DaysOfTheWeek类实现了 IEnumerable 接口,其需要一个GetEnumerator 方法。编译器会隐式的调用GetEnumerator方法来返回一个IEnumerator。
通过使用yield return 语句,GetEnumerator
方法每次返回一个字符串。
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]; } } }
如下的示例创建了一个包含Animals集合的Zoo类。
涉及到类实例的foreach语句隐式的调用GetEnumerator方法。涉及到Birds 和Mammals属性的foreach语句使用了命名的迭代器方法。
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 工具来查看其为一个迭代器方法生成的MSIL。
当你为一个类或者结构体创建一个迭代器的时,你不必实现完整的IEnumerator 接口。当编译器探测到迭代器时,它会自动生成 IEnumerator 或 IEnumerator<T> 接口的Current,
MoveNext,
和Dispose方法。
在foreach循环的每一次成功迭代时(或对IEnumerator.MoveNext的直接调用),在上一个yield return语句之后,下一个迭代器代码体便会重新开始,然后其继续到下一个yield return语句,知道到达迭代器的末尾,或者直到遇见了一个yield break语句。
迭代器不支持 IEnumerator.Reset 方法。为了从最开始进行迭代,你必须重新获取一个迭代器。在由迭代器方法返回的迭代器上调用Reset方法会抛出一个NotSupportedException 异常。
迭代器的使用
当你需要使用复杂的代码来便利一个列表序列时,迭代器使你可以维持foreach循环的简洁。
当你想要做如下事情时,这将会很有用:
- 在第一次foreach循环迭代之后,更改列表序列。
- 避免在foreach循环的第一次迭代之前完全加载一个大的列表序列。一个例子便是分页获取来加载一批数据行。另一个示例便是EnumerateFiles方法,其实现了.NET中的迭代器。
- 在迭代器中封装构建列表。在迭代器方法中,你可以构建列表,然后在循环中输出各个结果。