C#筆記 – 迭代器

  • 迭代器模式

    • 通過IEnumerator和IEnumerable接口以及它們的泛型等價物來封裝的

    • 如果某個類型實現了IEnumerable接口,就代表它是可迭代的

    • 調用GetIEnumerator方法將返回IEnumerator的實現(迭代器本身)

    • 可以把迭代器想象成數據庫的游標,迭代器只能在序列中向前移動,而且對於同一序列可能同時存在多個迭代器操作

      • 迭代器模式一個重要方面是,不用一次返回所有數據——調用代碼一次只需獲取一個元素。

      • 意味著我們需要確定訪問到了數組的哪個位置,因此需要一個「狀態值」

  • foreach語句實現了訪問迭代器的支持。foreach語句被編譯後會調用GetIEnumerator方法、Current屬性、MoveNext方法,假如IDisposable也實現了,程序最後還會自動銷毀迭代器對象

//C# 代碼
List<int> testForeach = new List<int> { 5, 8, 2, 3, 5 };
foreach (var i in testForeach)
{
   Console.WriteLine(i);
}
//IL 代碼
 IL_0030:  ldloc.0
 IL_0031:  callvirt   instance valuetype [System.Collections]System.Collections.Generic.List`1/Enumerator<!0> class [System.Collections]System.Collections.Generic.List`1<int32>::GetEnumerator() //調用Get IEnumerator
 IL_0036:  stloc.1
.try
{
   IL_0037:  br.s       IL_004a
   IL_0039:  ldloca.s   V_1
   IL_003b:  call       instance !0 valuetype [System.Collections]System.Collections.Generic.List`1/Enumerator<int32>::get_Current() //調用Current屬性
   IL_0040:  stloc.2
   IL_0041:  nop
   IL_0042:  ldloc.2
   IL_0043:  call       void [System.Console]System.Console::WriteLine(int32)
   IL_0048:  nop
   IL_0049:  nop
   IL_004a:  ldloca.s   V_1
   IL_004c:  call       instance bool valuetype [System.Collections]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext() //調用MoveNext方法
   IL_0051:  brtrue.s   IL_0039 //MoveNext == true,回到 IL_0039
   IL_0053:  leave.s    IL_0064 //MoveNext == false,退出至 IL_0064
}  // end .try
 finally
{
   IL_0055:  ldloca.s   V_1
   IL_0057:  constrained. valuetype [System.Collections]System.Collections.Generic.List`1/Enumerator<int32>
   IL_005d:  callvirt   instance void [System.Runtime]System.IDisposable::Dispose() //Dispose迭代器
   IL_0062:  nop
   IL_0063:  endfinally
}  // end handler
yield 與迭代器
  • 只要類實現了IEnumerable接口,那就代表它是可迭代的,並存在一個GetIEnumerator方法來返回一個IEnumerator接口

    • 只能使用迭代器塊來實現返回類型為IEnumerable, IEnumerator或泛型等價物的方法/屬性

      • 不能在匿名方法中使用迭代器代碼塊

    • 通過這個Enumerator中的MoveNext()來遍歷數據

  • public IEnumerator GetEnumerator()
    {
       for(int index = 0; index < values.Length; index++)
      {
           yield return values[(index + startingPoint) % values.Length];
      }
    }
  • yield return 告訴C#編譯器這是一個實現迭代器的方法而不是普通方法。

    • 如果GetIEnumerator返回的是一個非泛型的接口,那迭代器塊的生成類型(yield type)是object

    • 否則就是泛型接口的類型參數(IEnumerator => string)

  • yield return的限制

    • 如果存在任何catch代碼,則不能在try代碼塊中使用yield return

    • 并且在finally代碼塊中也不能使用 yield return 或 yield break

  • 在我們編寫迭代器塊時,儘管編寫了一個似乎是順序執行的方法,但實際上是請求編譯器為你創建了一個狀態機

    • 當編譯器看到迭代器塊時,會為狀態機創建一個嵌套類型,來正確記錄塊中的位置以及局部變量(包括參數)的值。

    • 實現迭代器,狀態機的工作:

      • 必須具有某個初始狀態

      • 每次調用MoveNext時,在提供下一個值之前(執行到yield return語句之前),它需要執行GetEnumerator方法中的代碼

        • 只有調用了IEnumerable.GetIEnumerator.MoveNext後,才會執行IEnumerable裡面的遍歷;初始化是不會執行

      • 使用Current屬性時,它必須返回我們生成的上一個值

        • 所有工作在調用MoveNext時就完成了,獲取Current的值不會執行任何代碼

        • 在第一次調用MoveNext之前,Current屬性總是返回迭代器產生類型的默認值

        • 在MoveNext返回false之後,Current屬性總是返回最後的生成值

      • 它必須知道何時完成生成值的操作,以便MoveNext返回false

  • yield return語句只表示「暫時地退出方法」,類似於「暫停」

    • 在yield return的位置,代碼就停止執行,在下一次調用MoveNext時又繼續執行

    • 在一個方法中的不同地方可以編寫多個yield return

    • 因此,代碼不會在最後的yield return處結束,而是通過返回false的MoveNext調用來結束方法的執行

延遲執行
  • 迭代器迭代時不會真的構造一個含有「從start開始,共count個」的數字的列表,而是在恰當的時間生成對應的那個數字

  • 構造的迭代器實例只是將東西準備好,使數據能在適當的位置以一種”just-in-time”的方式提供

  • 緩沖和流式技術

    • 框架提供的擴展方法會盡量嘗試對數據進行「流式(stream)」/「管道(pipe)」傳輸。

    • 要求一個迭代器提供下一個元素時,會從鏈接的迭代器中獲取一個元素,處理那個元素再返回符合要求的結果,不用占用自已更多的空間

      • 執行簡單的轉換和過濾時非常高效

      • 但執行反轉或排序等操作,就等如要求所有數據都處於可用狀態,所以需要加載所有數據到內存來執行批處理

    • 延遲執行與立即執行

      • Reference:https://stackoverflow.com/questions/2515796/deferred-execution-and-eager-evaluation

      • 設有一方法

        • int Computation(int index)
      • 延遲執行(deffered execution)可分為以下兩種

        • 流式傳輸 => 惰性求值(lazy evaluation)

          • IEnumerable<int> GetComputation(int maxIndex)
            {
               for(int i = 0; i < maxIndex; i++)
              {
                   yield return Computation(i);
              }
            }
          • 每次調用MoveNext才會調用一次Computation(i),將返回值放到Current中,然後返回Current

        • 緩沖傳輸 => 熱情求值(eager evaluation)

          • IEnumerable<int> GetComputation(int maxIndex)
            {
               var result = new int[maxIndex];
               for(int i = 0; i < maxIndex; i++)
              {
                   result[i] = Computation(i);
              }
               foreach(var value in result)
              {
                   yield return value;
              }
            }
          • 第一次調用MoveNext時,就會完成maxIndex次的Computation(i)方法,將結果存在result中,並返回第一個元素

          • 隨後的每次MoveNext,將會從array中取出下一位元素並放到Current中然後返回

      • 立即執行(immediately execution),相對於延遲執行

        • IEnumerable<int> GetComputation(int maxIndex)
          {
             var result = new int[maxIndex];
             for(int i = 0; i < maxIndex; i++)
            {
                 result[i] = Computation(i);
            }
             return result;
          }
        • 每次調用MoveNext時,會將放置在數組中的下一個值取出,放到Current中然後返回Current

      • 總結

        • 立即執行代表當方法return的時候,方法裡面的計算/執行肯定已經完成

        • 延遲/熱情求值代表大部分的工作會在首次MoveNext的時候完成

        • 延遲/惰性求值代表工作會在每次MoveNext調用時執行一次

迭代器的終止
  • yield break

  • finally代碼塊

    • 內置在foreach的IL代碼中

IL_0001:  call       class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32> CSharpInDepth.Program::CreateEnumerable()
IL_0006:  stloc.0
IL_0007:  nop
IL_0008:  ldloc.0
IL_0009:  callvirt   instance class [System.Runtime]System.Collections.Generic.IEnumerator`1<!0> class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
IL_000e:  stloc.1
.try
{
 IL_000f:  br.s       IL_0021
 IL_0011:  ldloc.1
 IL_0012:  callvirt   instance !0 class [System.Runtime]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
 IL_0017:  stloc.2
 IL_0018:  nop
 IL_0019:  ldloc.2
 IL_001a:  call       void [System.Console]System.Console::WriteLine(int32)
 IL_001f:  nop
 IL_0020:  nop
 IL_0021:  ldloc.1
 IL_0022:  callvirt   instance bool [System.Runtime]System.Collections.IEnumerator::MoveNext()
 IL_0027:  brtrue.s   IL_0011
 IL_0029:  leave.s    IL_0036
}  // end .try
finally
{
 IL_002b:  ldloc.1
 IL_002c:  brfalse.s  IL_0035
 IL_002e:  ldloc.1
 IL_002f:  callvirt   instance void [System.Runtime]System.IDisposable::Dispose()
 IL_0034:  nop
 IL_0035:  endfinally
}  // end handler
    • 在遇到yield return時,只是暫停了方法,因此不會執行finally裡的代碼

    • 而遇到yield break時,適當的finally代碼塊會被執行

      • finally在迭代器塊中常用於釋放資源,通常與using語句配合使用

    • 而假如我們不是提前停止執行迭代器代碼,而是提前停止使用迭代器,finally代碼塊裡的內容也會執行

      • IL代碼中仍然會出現finally塊

      • DateTime stop = DateTime.Now.AddSeconds(2);
        foreach(int i in CountWithTimeLimit(stop))
        {
           if(i > 3)
          {
               //提前停止使用迭代器
               return;
          }
        }
      • //IL 代碼
        finally
        {
           IL_002b:  ldloc.1
           IL_002c:  brfalse.s  IL_0035
           IL_002e:  ldloc.1
           IL_002f:  callvirt   instance void [System.Runtime]System.IDisposable::Dispose()
           IL_0034:  nop
           IL_0035:  endfinally
        }  // end handler
      • 這是由於foreach會在它自己的finally代碼塊中調用IEnumerator所提供的Dispose方法

        • 只要調用者使用了foreach循環,迭代器塊中的finally就會執行

    • 反之,只要我們不通過foreach,而是手動調用MoveNext,finally就不會被觸發

      • IL_0008:  callvirt   instance class [System.Runtime]System.Collections.Generic.IEnumerator`1<!0> class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
        IL_000d:  stloc.1
        IL_000e:  ldloc.1
        IL_000f:  callvirt   instance bool [System.Runtime]System.Collections.IEnumerator::MoveNext()
        IL_0014:  pop
        IL_0015:  ldloc.1
        IL_0016:  callvirt   instance bool [System.Runtime]System.Collections.IEnumerator::MoveNext()
        IL_001b:  pop
        IL_001c:  call       valuetype [System.Console]System.ConsoleKeyInfo [System.Console]System.Console::ReadKey()
      • 除非我們再手動顯式調用Dispose方法,那finally方法就會被執行


參考書目

  • 《CLR via C#》(第4版) Jeffrey Richter
  • 《深入理解C#》(第3版) Jon Skeet