-
迭代器模式
-
通過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