c#列举和迭代器
列举 - Enumeration
迭代器是一个值序列(集合)上的一个只读且只向前移动的游标。迭代器要么实现了IEnumerator接口,要么实现了IEnumerator<T>接口。
从技术的角度看,如果一个对象有MoveNext方法以及Current属性,那么我们就可以将其看作一个迭代器。
我们可以使用foreach语句去迭代一个可列举对象。可迭代的对象其实就是一个序列的逻辑体现。可列举的对象不但自身就是一个游标,而且它还可以生成一个游标迭代自己。因此,可列举的对象有两个特性
- 实现IEnumerator接口,或实现IEnumerator<T>接口
- 有一个方法GetEnumerator,该方法返回一个迭代器
列举模式:
class Enumerator { public IteratorVariableType Current {get {...}} public bool MoveNext() {...} } class Enumerable { public Enumerator GetEnumerator() {...} }
为了更好的理解上面的概率和模式,我们来看下面的两个例子
foreach (char c in "CSharp") Console.WriteLine(c);
using (var enumerator = "CSharp".GetEnumerator()) { while (enumerator.MoveNext()) { Console.WriteLine(enumerator.Current); } }
Sample1采取了foreach这样的高级方式去迭代字符串(因为字符串类实现了CharEnumerator);而Sample2则使用了底层的方式完成对字符串的迭代。 对于Sample我们使用了using语句,这是因为CharEnumerator实现了IDisposable接口,下面的代码显示了CharEnumrator的大部分代码(来自微软官方)
public sealed class CharEnumerator : IEnumerator, IDisposable { private String str; private int index; private char currentElement; internal CharEnumerator(String str) { this.str = str; this.index = -1; } public bool MoveNext() { if (index < (str.Length - 1)) { index++; currentElement = str[index]; return true; } else index = str.Length; return false; } public void Dispose() { if (str != null) index = str.Length; str = null; } public char Current { get { return currentElement; } } public void Reset() { currentElement = (char)0; index = -1; } }
初始化集合
我们可使用一行语句实例一个可列举的对象。比如:IList<Int> list = new List<int>{1,2,3};编译时,编译器会自动翻译为:
IList<Int> list = new List<int>(); list.Add(1); list.Add(2); list.Add(3);
这是因为该列举对象实现了IEnumerable接口,而且还包含了Add方法。
为了验证此点,我们可以通过查看IL代码的方式来确认:
IL_0000: nop IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor() IL_0006: stloc.1 IL_0007: ldloc.1 IL_0008: ldc.i4.1 IL_0009: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0) IL_000e: nop IL_000f: ldloc.1 IL_0010: ldc.i4.2 IL_0011: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0) IL_0016: nop IL_0017: ldloc.1 IL_0018: ldc.i4.3 IL_0019: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0) IL_001e: nop IL_001f: ldloc.1 IL_0020: stloc.0 IL_0021: call string [mscorlib]System.Console::ReadLine()
迭代器 - Iterator
既然foreach可应用于列举,那么一个列举可以生成一个迭代器。很绕口很困惑是吧,我们先来看下面的例子:使用迭代器返回斐波纳契数列
static IEnumerable<int> Fibonacci(int number) { for(int i=0, prevFib=1, curFib=1;i<number;i++) { yield return prevFib; int newFib = prevFib + curFib; prevFib = curFib; curFib = newFib; } } // test static void Main(string[] args) { foreach (int f in Fibonacci(10)) Console.WriteLine(f); Console.ReadLine(); }
请注意,在上面的代码中,我们使用了yield return。那么它和return有什么区别呢?
return:从方法中返回一个值
yield return:从当前的迭代器中生成下一个元素。yield语句每执行一次,程序的控制权就退还给调用者,而被调用者的状态仍然保留,这就使得方法在调用者列举下一个元素的时候能继续执行。被调用者的状态的生命周期取决于列举,正因为如此,当调用者完成列举后,被调用者的状态得以释放。
迭代器语法
迭代器可以是包含了一个或多个yield语句的方法、属性、或所引器。迭代器必须返回下面四个类型之一:IEnumerable, IEnumerable<T>, IEnumerator, IEnumerator<T>
再继续下一步之前,我们看一下IEnumerable接口和IEnumerator的定义
public interface IEnumerator { bool MoveNext(); Object Current {get; } void Reset(); } public interface IEnumerable { IEnumerator GetEnumerator(); }
迭代器与列举有不一样的语法,在于迭代器需要返回可列举的接口或者列举器接口。
创建序列
迭代器可以进一步用于创建迭代。为了证实这点,我们可以扩展我们斐波纳契数列例子
static IEnumerable<int> Fibonacci(int number) { for(int i=0, prevFib=1, curFib=1;i<number;i++) { yield return prevFib; int newFib = prevFib + curFib; prevFib = curFib; curFib = newFib; } } static IEnumerable<int> EvenNumbers(IEnumerable<int> sequence) { foreach (int x in sequence) if (x % 2 == 0) yield return x; } static void Main(string[] args) { foreach (int f in EvenNumbers(Fibonacci(8))) Console.WriteLine(f); Console.ReadLine(); }
请注意,直到Fibonacci方法所产生的数列的MoveNext()方法被调用时(执行foreach循环,会隐式地调用IEnumerator的MoveNext方法),才会判断该元素是否为偶数。
迭代器可以进一步用于创建迭代大量应用于LINQ。