[.NET]快快樂樂學LINQ系列前哨戰-IEnumerable, IEnumerator, yield, Iterator

[.NET]快快樂樂學LINQ系列前哨戰-IEnumerable, IEnumerator, yield, Iterator

前言

LINQ的基礎,牽扯到幾個長的很像的東西,例如IEnumerable, Enumerable, IEnumerator, 還加上對應的泛型介面IEnumerable<T>, IEnumerator<T>,還有IEnumerable裡面的方法GetEnumerator()會回傳IEnumerator,IEnumerable<T>裡面的方法GetEnumerator()則會回傳IEnumerator<T>。

這幾個東西到底是在搞什麼鬼,在這篇文章以及下篇文章,希望透過文章的介紹,可以讓讀者更瞭解它們之間的關係。

 

關係圖

先用一張簡單的圖來說明foreach, IEnumerable, IEnumerable<T>, IEnumerator與IEnumerator<T>的關係。

image

接下來將先逐一說明foreach的相關原理。

 

說明

有寫過程式的朋友,幾乎一定用過foreach迴圈來針對某一個物件集合,進行逐一巡覽的動作,而且覺得這樣的程式再自然不過了。但是,其實foreach幫忙簡化了很多的動作,我們來看透过IL看C# (3)-foreach语句這篇文章所提到的兩段程式碼。

第一段是foreach的sample:

static void Test(ICollection<int> values)
{
    foreach(int i in values)
        Console.WriteLine(i);
}

接下來是這個Test方法的IL程式碼:

.method private hidebysig static void  Test(class [mscorlib]System.Collections.Generic.ICollection`1<int32> values) cil managed
{
  // 代码大小       55 (0x37)
  .maxstack  2
  .locals init (int32 V_0,    // i
           class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> V_1,
           bool V_2)
  IL_0000:  nop
  IL_0001:  nop
  
  // V_1 = (IEnumerator<int>)values.GetEnumerator()
  IL_0002:  ldarg.0
  IL_0003:  callvirt   instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
  IL_0008:  stloc.1
  
  .try
  {
    // goto IL_0019
    IL_0009:  br.s       IL_0019
    
    // V_0 = V_1.Current
    IL_000b:  ldloc.1
    IL_000c:  callvirt   instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
    IL_0011:  stloc.0
    
    // Console.WriteLine(V_0)
    IL_0012:  ldloc.0
    IL_0013:  call       void [mscorlib]System.Console::WriteLine(int32)
    IL_0018:  nop
    
    // V_2 = V_1.MoveNext()
    IL_0019:  ldloc.1
    IL_001a:  callvirt   instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
    IL_001f:  stloc.2
    
    // if(V_2) goto IL_000b else goto IL_0035 //(IL_0035===ret)
    IL_0020:  ldloc.2
    IL_0021:  brtrue.s   IL_000b
    IL_0023:  leave.s    IL_0035
  }  // end .try
  finally
  {
    // if(V_1 != null) V_1.Dispose()
    IL_0025:  ldloc.1
    IL_0026:  ldnull
    IL_0027:  ceq
    IL_0029:  stloc.2
    IL_002a:  ldloc.2
    IL_002b:  brtrue.s   IL_0034
    IL_002d:  ldloc.1
    IL_002e:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_0033:  nop
    IL_0034:  endfinally
  }  // end handler
  IL_0035:  nop
  IL_0036:  ret
} // end of method Program::Test

看不懂IL程式碼,沒關係,上面的註解寫得相當清楚,請讀者把前面關係圖裡面的關鍵字找出來即可。

簡單的說,foreach的in所使用的物件,必須實作IEnumerable或IEnumerable<T>。

為什麼in所使用的物件,必須實作IEnumerable呢?在這例子中,foreach的底層運作,

  1. 從IL上第11行的註解可以看到,會先去呼叫values.GetEnumerator()這個IEnumerable介面上所宣告的方法,取得IEnumerator<int>放到V_1中。
  2. 接著21行中,會取得IEnumerator<T>的Current屬性,得到目前的項目。
  3. 接著26行,指的是進入foreach迴圈的本體,也就是Test方法那段sample 的第4行:Console.WriteLine(i);
  4. 迴圈本體執行完後,第31行可以看到,呼叫IEnumerator<int>的MoveNext()方法,這時values集合中的index會指到下一個項目的index,並得到一個bool值。bool若為true,則代表還有下一個項目。
  5. 若MoveNext()方法得到的結果為true,則goto IL_000b,也就是第22行,也就是所謂的下一圈迴圈。若為false,則跳到IL_0035,也就是第55行。跳出/結束迴圈。

透過上面簡單的說明,可以瞭解到foreach的逐一巡覽,其實幫我們隱藏了IEnumerator的相關動作。

而我們常用到的集合結構,例如ICollection, IDictionary, IList, 以及其泛型介面,實作的類別,以及實作的泛型類別,都有實作IEnumerable或IEnumerable<T>,所以這些集合都可以透過foreach來展開逐一巡覽的動作。

然而,那又如何?foreach本來就是這樣用了,多瞭解了背後運作原理,相信我,十年後我連IEnumerator怎麼拼都不會拼,我還是可以活的好好的。跟yield有什麼關係呢?

只有用.net framework內建的類別,當然看不太出來,因為最麻煩的是在:

  1. 每一個想要被foreach逐一巡覽的類別,都要實作IEnumerable介面,實作GetEnumerator方法。
  2. 接下來還要自己新增一個類別,來實作IEnumerator介面,並且實作MoveNext(), Reset()與Current屬性,以供GetEnumerator方法回傳。

到這邊,是不是覺得很麻煩,而且覺得怎麼可能需要這麼麻煩?可以參考一下Clark的[.NET] Lazy Row Mapping這篇文章,這個時候讀者應該就可以看懂,那一堆IEnumerator<T>跟IEnumerable<T>是在幹嘛了。文章的中間有提到,可以透過yield來實作這堆麻煩事,接下來,就來介紹yield可以幫我們省掉什麼麻煩。而在講yield之前,要先說明一下什麼是Iterator。

 

Iterator與yield

先來看MSDN很重要的兩句話:

1. 您只要提供 Iterator,它會只往返於類別中的資料結構。當編譯器偵測到您的 Iterator 時,它會自動產生 IEnumerable 或 IEnumerable<T> 介面的 Current、MoveNext 和 Dispose 方法。

2. Iterator 程式碼會使用 yield return 陳述式輪流傳回各元素。yield break 則會結束反覆運算。

針對第一點最後的描述,個人覺得MSDN上說的不夠清楚,應該是:『它會自動產生IEnumerable或IEnumerable<T>介面中,GetEnumerator方法裡面取得的IEnumerator或IEnumerator<T>介面中的Current、MoveNext和Dispose方法。』不曉得是我誤解字面上的意思,還是筆誤,還是太繞口了。

接著來看MSDN上,Iterator使用yield return來完成IEnumerable介面的GetEnumrator方法內容。

使用yield:

public class DaysOfTheWeek : System.Collections.IEnumerable
{
    string[] m_Days = { "Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat" };

    public System.Collections.IEnumerator GetEnumerator()
    {
        for (int i = 0; i < m_Days.Length; i++)
        {
            yield return m_Days[i];
        }
    }
}

class TestDaysOfTheWeek
{
    static void Main()
    {
        // Create an instance of the collection class
        DaysOfTheWeek week = new DaysOfTheWeek();

        // Iterate with foreach
        foreach (string day in week)
        {
            System.Console.Write(day + " ");
        }
    }
}

這樣似乎看不太出來,yield return做了什麼事,這邊我用一模一樣的例子,簡單地模擬一下如果沒有yield,程式碼會變得多麻煩:

    public class DaysOfTheWeek : IEnumerable
    {
        private string[] m_Days = { "Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat" };

        public IEnumerator GetEnumerator()
        {
            var result = new DaysOfTheWeek_Enumerator(m_Days);
            return result;
        }
    }

    //要自己新增一個class,並且實作Current, MoveNext, Reset
    //若是透過yield來處理,則省了一個class,以及逐一巡覽的實作過程
    public class DaysOfTheWeek_Enumerator : IEnumerator
    {
        private int index = -1;
        private string[] m_Days;

        public DaysOfTheWeek_Enumerator(string[] days)
        {
            m_Days = days;
        }

        public object Current
        {
            get { return m_Days[index]; }
        }

        public bool MoveNext()
        {
            index++;
            return (index < m_Days.Length);
        }

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

    internal class TestDaysOfTheWeek
    {
        private static void Main()
        {
            // Create an instance of the collection class
            DaysOfTheWeek week = new DaysOfTheWeek();

            // Iterate with foreach
            foreach (string day in week)
            {
                System.Console.Write(day + " ");
            }
        }
    }

順便補充一下,Iterator其實是一種design pattern,有興趣的朋友可以參考一下:Iterator Pattern

還有,MSDN上有提到yield return的回傳型別種類:

Iterator 的傳回型別必須是 IEnumerable、IEnumerator、IEnumerable<T> 或 IEnumerator<T>。

 

yield 範例

這邊透過範例,來讓讀者朋友瞭解到,iterator在使用yield return 和yield break,與foreach展開IEnumerable物件的關係與順序性。

    internal class Program
    {
        private static IEnumerable<string> GetEnumeratorFromDays(string[] days)
        {
            foreach (var day in days)
            {
                Console.WriteLine("yield return前, 準備回傳day為:{0}", day);
                if (day == "3")
                {
                    Console.WriteLine("day為3,呼叫yield break");
                    yield break;
                }

                yield return day;
                Console.WriteLine("yield下一行, 準備回傳day為:{0}", day);
            }
        }

        private static void Main(string[] args)
        {
            string[] days = { "0", "1", "2", "3", "4", "5", "6" };

            var result = GetEnumeratorFromDays(days);

            foreach (var item in result)
            {
                Console.WriteLine("實際展開的result item:{0}", item);
                Console.WriteLine("item:{0}處理完畢,準備跳下一個item", item);
                Console.WriteLine();
            }

            Console.WriteLine("結束result");
        }
    }

先來看一下結果:

image

重點:

  1. 要建立一個IEnumerable<T>的資料集合,只要使用yield return就可以輕鬆達成。
  2. 而yield return這次的項目後,下一次被呼叫時,會接著從剛剛yield return後開始執行,而不是GetEnumeratorFromDays()重頭執行唷。
  3. 呼叫yield break後,會直接結束foreach迴圈,就類似於Iterator的break。

 

結論

本來只是想快快樂樂的說一下yield return與yield break的用法,沒想到一路帶回到foreach的基本知識。不曉得會不會違背了原本快快樂樂的主題。

但越簡單的東西,真的越難說明白。

讀者朋友們也可以算一下,自己用過了多少次的foreach,MSDN上也都有相關的資料,但您真的有去瞭解背後的原理嗎?

最後,用.NET開發是幸福的,因為這種繁瑣、重複的動作,都被幫忙處理掉了。至於變成一位.NET工程師,會不會可悲,會不會根本沒去瞭解背後的運作原理,就看各位自己的學習心態囉。

Reference

  1. foreach、in (C# 參考)
  2. IEnumerable 介面
  3. IEnumerable 泛型介面
  4. IEnumerator 介面
  5. IEnumerator<T> 介面
  6. Iterator (C# 程式設計手冊)
  7. yield (C# 參考)
  8. Iterator Pattern
  9. 透过IL看C# (3)——foreach语句
  10. 談C# 編譯器編譯前的程式碼擴展行為

或許您會對下列培訓課程感興趣:

  1. 2019/6/15(六)~2019/6/16(日):工程實踐與流程規範導入實務 201906 第一梯次(台北)
  2. 2019/7/27(六)~2019/7/28(日):演化式設計:測試驅動開發與持續重構 第六梯次(台北)
  3. 2019/8/16(五)~2019/8/18(日):【C#進階設計-從重構學會高易用性與高彈性API設計】(台北)
  4. 2019/10/19(六):【針對遺留代碼加入單元測試的藝術】(台北)
  5. 2019/10/20(日):【極速開發】第八梯次(台北)

想收到第一手公開培訓課程資訊,或想詢問企業內訓、顧問、教練、諮詢服務的,請洽 Facebook 粉絲專頁:91敏捷開發之路