1. 程式人生 > >c# 迭代器與yield關鍵字解析

c# 迭代器與yield關鍵字解析

相信好多程式設計師都是因為unity的協程(Coroutine)認識yield這個關鍵字的,知道在unity的開發中諸如yield return null、yield return new WaitForSeconds(1.0f)的用法,其實yield是C#的關鍵字,unity的協程只是在c#的基礎上做了一層封裝,我們現在來看看yield這個關鍵字。

說到yield就不得不說迭代器,迭代器模式是設計模式的一種,因為其運用的普遍性,很多語言都有內嵌的原生支援。在.NET中,迭代器模式是通過IEnumerator、IEnumerable兩個介面和兩個同名的泛型介面來封裝的:

public interface IEnumerator
    {
        object Current
        {
            get;
        }

        bool MoveNext();
        void Reset();
    }
IEnumerator只定義了一個屬性、兩個函式,Current為迭代器的當前值,通過呼叫MoveNext函式讓迭代器的前進一步,返回值表示該迭代器是否結束,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塊中。