由于大多数程序的算法都是要在一系列元素而非单一元素上执行操作,因此开发者会使用 foreach、for 循环及 while 等结构。通常把某集合用作输入值,然后检视或修改集合本身或其中元素,最后把另一个集合作为输出值返回给调用方。
这样做的问题在于,若针对整个集合中的每个元素执行操作,程序效率很低。因为执行的操作通常不止一个,且需要多次变换才能把源集合元素转换为目标集合元素。在此过程中,需要创建一些集合保存中间结果,且集合有可能较大,必须等整个集合完成了一次变换操作后,才能继续执行下一次变换操作。要执行几次操作,就得把集合遍历几遍,因此,若执行操作较多,那么算法的执行时间会较长。
另一种办法是,在方法中仅遍历一次,将序列中每个元素都处理一遍,并对其进行各种变换,得到结果。这将提高程序的效率,降低内存使用 (不用每执行一步就创建一个集合)。但这样的的代码很难复用,因为开发者复用的不是整套逻辑,而是其中的一小步。
由于 C# 有迭代器 (iterator),因此,开发者可用它创建出一种方法来操作序列中的元素,这样的方法只会在调用方法真正请求获取元素是才会处理并返回该元素。这些方法以 IEnumerable
如下实例将序列中每种元素输出一次 (重复元素不输出):
1 | public static void Unique(IEnumerable<int> nums) |
此方法虽然简单,但是不能进行复用。
可以考虑改用迭代器方法:
1 | public static IEnumerable<int> UniqueV2(IEnumerable<int> nums) |
有人认为这样改差不多,没什么好处。加上一些追踪语句,能让你更清楚此方法的运作:
1 | public static IEnumerable<int> UniqueV2(IEnumerable<int> nums) |
之所以有这样的效果,关键就在于 yield return 语句。此语句会返回值,并保留信息,记录当前执行的位置及内部迭代逻辑的状态。用此语句写出来的方法,输入输出值都是迭代器,其迭代逻辑可根据早前保留的信息判断当前应读取输入序列的哪一元素,据此生成并返回输出序列中的下一元素。此方法属于可从上次执行位置继续执行的方法 (continuable method),系统每次运行它时,可根据先前记录的状态信息决定继续执行的位置。
将 Unique() 方法改写成连续方法 (continuation method) 有两个好处:
- 推迟了每个元素的求值时机,提高程序效率。
- 此操作可拼接,可灵活复用。
反之,想用包含 foreach 循环的命令式方法进行灵活复用则较为困难。
注意,Unique() 方法还可转换为泛型方法:
1 | public static IEnumerable<T> UniqueV3<T>(IEnumerable<T> sequence) |
迭代器方法可把多个步骤拼接成一套流程。若要输出是每一种数值的平方,接上一个 Square() 即可:
1 | public static IEnumerable<int> Square(IEnumerable<int> nums) |
无论使用多少个迭代器方法,仅需将源序列迭代一次即可。
将序列用作迭代器的输入参数。并令其输出另一序列是一种很好的思路,这使得开发者能设计更多的用法。若迭代器方法的参数不是一个序列而是两个,可用这样的迭代器方法将两个序列合并起来:
1 | public static IEnumerable<string> Zip(IEnumerable<string> first, IEnumerable<string> second) |
Zip() 从两个不同的字符串序列中分别取出一个元素,并连接成为新字符串,输出目标序列。当然,此方法也可设计成泛型方法,只不过稍复杂 (见18条)。
迭代器方法不会修改源序列本身,而是会依次产生目标序列中的元素,这些元素构成一个新序列。若源序列中的元素是引用型,那么迭代器有可能在处理元素时改动该元素内容。
如果能把复杂的算法拆解成多个步骤,并将每个步骤都表示成小型的迭代器方法,那么可将这些方法拼成一条管道,使得程序仅需遍历一次源序列处理,即可对其中元素进行多种小变换。