c# 迭代器與yield關鍵字解析
相信好多程式設計師都是因為unity的協程(Coroutine)認識yield這個關鍵字的,知道在unity的開發中諸如yield return null、yield return new WaitForSeconds(1.0f)的用法,其實yield是C#的關鍵字,unity的協程只是在c#的基礎上做了一層封裝,我們現在來看看yield這個關鍵字。
說到yield就不得不說迭代器,迭代器模式是設計模式的一種,因為其運用的普遍性,很多語言都有內嵌的原生支援。在.NET中,迭代器模式是通過IEnumerator、IEnumerable兩個介面和兩個同名的泛型介面來封裝的:
IEnumerator只定義了一個屬性、兩個函式,Current為迭代器的當前值,通過呼叫MoveNext函式讓迭代器的前進一步,返回值表示該迭代器是否結束,Reset函式用於重置資料。public interface IEnumerator { object Current { get; } bool MoveNext(); void Reset(); }
public interface IEnumerable
{
[DispId(-4)]
IEnumerator GetEnumerator();
}
IEnumerable更簡單,返回迭代器。一般這兩個介面的實現位於不同的類中。foreach關鍵字之所以能方便對陣列、List、Dictionary進行迴圈,其實也是在背後呼叫IEnumarator的MoveNext函式從頭遍歷到尾,取出每次的Current值,說白了它是個語法糖,在編譯後會對我們的程式碼自動替換。我們來看下List的迭代器實現:
public struct Enumerator : IEnumerator<T>, System.Collections.IEnumerator { private List<T> list; private int index; private int version; private T current; internal Enumerator(List<T> list) { this.list = list; index = 0; version = list._version; current = default(T); } public void Dispose() { } public bool MoveNext() { List<T> localList = list; if (version == localList._version && ((uint)index < (uint)localList._size)) { current = localList._items[index]; index++; return true; } return MoveNextRare(); } private bool MoveNextRare() { if (version != list._version) { ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion); } index = list._size + 1; current = default(T); return false; } public T Current { get { return current; } } Object System.Collections.IEnumerator.Current { get { if( index == 0 || index == list._size + 1) { ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumOpCantHappen); } return Current; } } void System.Collections.IEnumerator.Reset() { if (version != list._version) { ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion); } index = 0; current = default(T); } }
public class List<T> : IEnumerable, ICollection, IList, ICollection<T>, IEnumerable<T>, IList<T>
{
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return new Enumerator(this);
}
}
可以看到其實現是規規矩矩的繼承了IEnumerator、IEnumerable及其兩個泛型介面,一切都很完美,只有一個問題,是什麼問題呢?答:寫的太累了(手動滑稽)。終於引出了yield,沒錯,yield可以大大的簡化迭代器程式碼,讓Coder寫起來更加輕鬆自在,我們的迭代程式碼可以這樣寫: public class Iteration: IEnumerable
{
public List<int> lstInfo = new List<int>() { 1, 3, 5, 7, 9, 11 };
public IEnumerator GetEnumerator()
{
for (int i = 0; i < lstInfo.Count; ++i)
{
yield return lstInfo[i];
}
}
}
對於使用者來說方式還是一樣: static void IterationTest()
{
Iteration obj = new Iteration();
foreach (var item in obj)
{
Console.WriteLine(item);
}
}
當然啦,List的迭代器程式碼還是好多是關於版本號判斷的,我們的示例並沒有相關的邏輯,不過就算是加上,程式碼依然可以精簡很多,這就是yield的魅力所在。有些人看到這可能還是迷惑,因為大部分的程式設計師的思路都是線性的,上面的Iteration類的GetEnumerator函式的for迴圈不是一下都遍歷完了嗎,怎麼還能給foreach用,好蒙啊。。。yield很神奇吧?是這樣的:Jon Skeet說:“迭代器模式的一個重要方面就是:不用一次返回所有資料,呼叫程式碼一次只需獲取一個元素。”你可以理解為每次執行yield return都能夠返回一個數據並暫停當前的狀態,那暫停的狀態什麼時候會繼續呢?在下一次呼叫到MoveNext的時候。什麼時候會呼叫MoveNext?foreach執行完一次,進入下一次的時候。
如果還不是很明白,我們再來看看《c# in Depth》的經典例子:
class IteratorWorkflow
{
static readonly string Padding = new string(' ', 30);
static IEnumerable<int> GetEnumerable()
{
Console.WriteLine("{0}Start of GetEnumerator()", Padding);
for (int i = 0; i < 3; i++)
{
Console.WriteLine("{0}About to yield {1}", Padding, i);
yield return i;
Console.WriteLine("{0}After yield", Padding);
}
Console.WriteLine("{0}Yielding final value", Padding);
yield return -1;
Console.WriteLine("{0}End of GetEnumerator()", Padding);
}
public static void Main()
{
IEnumerable<int> iterable = GetEnumerable();
IEnumerator<int> iterator = iterable.GetEnumerator();
Console.WriteLine("Starting to iterate");
while (true)
{
Console.WriteLine("Calling MoveNext()...");
bool result = iterator.MoveNext();
Console.WriteLine("... MoveNext result={0}", result);
if (!result)
{
break;
}
Console.WriteLine("Fetching Current...");
Console.WriteLine("... Current result={0}", iterator.Current);
}
}
}
輸出的結果為:我相信,如果你有對照著這個例子認真分析一遍的話,應該就能掌握yield這個知識點了,如果還不清楚,程式碼Copy下來,自己跑一遍~~
最後有幾個知識點總結歸納一下:
1·在遇到yield break或者返回IEnumerator的函式體結束前,不管yield return 的值為多少,MoveNext都是會返回True。
2·在第一次呼叫MoveNext之前,返回IEnumerable的程式碼都不會執行,即使你有主動去呼叫它。
3·執行到yield return的地方,程式碼就暫停了,並返回相應的值,在下一次呼叫MoveNext時,從上次暫停的地方繼續執行。
4·yield return 程式碼不能放入try...catch塊中,但是能放入try...finally塊中。