《CSharp In Depth》读书笔记(6)——Iterators

Iterator的一般用法

在CSharp1中foreach可以接受任何实现了IEnumerable的类。 在经过编译之后,foreach将自动的调用GetEnumerator得到实现了IEnumerator的迭代器。 通过不断的调用迭代器的MoveNext/Current保证每次迭代传给循环体新的经过迭代的值。

下面是一个简单的IEnumerator的实现,这个迭代器可以在一个环形数组上迭代。

class IterationSampleIterator : IEnumerator
{
	IterationSample parent;
	int position;
	
	internal IterationSampleIterator(IterationSample parent)
	{
		this.parent = parent;
		position = -1;
	}

	public bool MoveNext()
	{
		if (position != parent.values.Length)
		{
			position++;
		}
		return position < parent.value.Length;
	}

	public object Current
	{
		get
		{
			if (position == -1 || position == parent.value.Length) 
			{
				throw new InvalidOperationException();
			}
			int index = position + parent.startingPoint;
			index = index % parent.values.Length;
			return parent.values[index];
		}
	}

	public void Reset()
	{
		position = -1;
	}
}

Iterators with yield statements

迭代器的设计初衷虽然很好,但是需要程序员自己实现大量的迭代器逻辑还是很麻烦。 于是CSharp2中引入了yield关键字。 yield的出现大幅简化了实现一个迭代器所需要的代码。

再来看下边的代码:

public IEnumerator GetEnumerator()
{
	for (int index = 0; index < value.Length; index++)
	{
		yield return values[(index + startingPoint) % values.Length];
	}
}

上边这段代码看上去像是一个不同的循环语句, 但实际上编译器为我们创建了一个状态机。 这个状态机的作用基本等同于上边那个迭代器的作用:

当然上述的操作都是在编译器内部帮我们实现的。 而这一切的关键就是yield。

yield return

yield return的作用相当于将执行“暂停”。 下次同一段代码再次被执行的时候则从上次被执行到的地方继续开始。 当然,这里的“执行”指的是MoveNext被调用的时候。 可以这样理解:yield return指令将一段代码划分成为若干个可迭代的部分。 每一次代码的迭代都会从上一次yield return的地方开始执行,直到遇到下一个yield return的时候终止。 这时候Current返回的是本次yield return的值。

yield break

yield break的作用类似于break在循环中体的作用。 其实就是要break(打破)迭代(循环)的意思。 因此在函数中加入yield break可以直接使得MoveNext返回false,结束迭代。

其他的注意事项

在使用yield指令自动生成的迭代器中,有一些需要注意的细节,罗列如下:

iterator的应用

封装复杂的循环条件

比如下边的例子,用yield简单实现的iterator可以将复杂的循环条件封装起来。 调用的时候只要使用foreach就可以了。

// 使用默认的for循环
for (DateTime day = StartDate; day <= timetable.EndDate; day = day.AddDays(1))
{
	// ...
}

// 使用iterator
public IEnumerable<DateTime> DateRange
{
	get
	{
		for (DateTime day = StartDate; day <= EndDate; day = day.AddDays(1))
		{
			yield return day;
		}
	}
}

foreach (DateTime dt in DateRange)
{
	// ...
}

用iterator进行过滤

因为Current只会返回yield return的值, 所以我们可以利用这一个特性选择性的返回一个集合中的元素。 这样的特性恰好是一个filter所需要的。

// Implementing LINQ's where method using iterator blocks
public static IEnumerable<T> Where<T>(IEnumerable<T> source, Predicate<T> predicate)
{
	if (source == null || predicate == null)
	{
	   throw new ArgumentNullException();
	}
    return WhereImpl(source, predicate);
}

// Internal implementation
private static IEnumerable<T> WhereImpl<T>(IEnumerable<T> source, Predicate<T> predicate)
{
	foreach (T item in source)
	{
		if (predicate(item))
		{
			yield return item;
		}
	}
}

// Use like this
Predicate<string> predicate = (string line) => { return line.StartsWith("using");}; 

foreach (string line in Where(lines, predicate))
{
	Console.WriteLine(line);
}

伪同步的CCR与Coroutine

iterator与yield另外一个重要的意义是在于提供了一暂停/重启执行的异步逻辑框架。 Microsoft提供了Concurrency and Coordination Runtime(CCR)这个Library。 在Unity中也提供了类似的机制——Coroutine。

二者在代码大概是这个感觉:

// Sample using CCR
static IEnumerator<ITask> ComputeTotalStockVal.(str.user,str.pass)
{
	string token = null;
	
	yield return Arbiter.Receive(false, AuthService.CcrCheck(user, pass),
		delegate(string t) { token = t; });
	
	IEnumerable<Holding> stocks = null;
	IDictionary<string,decimal> rates = null;
	
	yield return Arbiter.JoinedReceive(false,
		DbService.CcrGetStockHoldings(token),
		StockService.CcrGetRates(token),
		delegate(IEnumerable<Holding> s, IDictionary<string,decimal> r)
			{ stocks = s; rates = r; });

	OnRequestComplete(ComputeTotal(stocks, rates));		
}

// Sample using Unity Coroutine
public IEnumerator Task()
{
	// wait for next frame
	yield return null;

	WWW www = new WWW("http://file-to-download");

	// wait until download complete
	yield return www;

	// handle downloaded resources
}

iterator和yield极大地降低了实现异步逻辑的成本。

小结