--- tags: LinQ, LinQ基礎 , C# --- # LinQ基礎 - 延遲執行(Deferred Execution) ### 延遲執行的基礎 - 疊代器 & 走訪 詳細請參考下列文章. - [LINQ基礎 - Iterator(疊代器模式)](https://hackmd.io/vnZcWNdGRCq1cjMJRMJZZA) - [LINQ基礎 - IEnumerable & IEnumerator](https://hackmd.io/lSm0-BylRdK-MV0AErhHyw) - [LINQ基礎 - Yield](https://hackmd.io/P_h9ag3ETIOuZMrCq41hWQ?view) 這邊會用一個例子快速回顧 , 以下是自定義類別 ```C# public class CityCollection : IEnumerable { public IEnumerator GetEnumerator() { Console.WriteLine("item - 1"); yield return 1; Console.WriteLine("item - 2"); yield return 2; Console.WriteLine("item - 3"); yield return 3; Console.WriteLine("item - finish"); } } ``` 以下是執行 foreach 的程式 ```C# static void Main(string[] args) { var city = new CityCollection(); Console.WriteLine("foreach start"); foreach (var item in city) { Console.WriteLine("foreach item :" + item); } Console.WriteLine("foreach end"); Console.ReadKey(); } ``` 以下是運行結果 ![2oVy9cd.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/2oVy9cd.png?raw=true) 使用上面例子 , 利用 Visual Studio 去逐步偵錯, 可以知道 foreach 的執行順序其實是 1. 進入 foreach - ![WnBR9uY.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/WnBR9uY.png?raw=true) 2. 執行 GetEnumerator() , 得到一個 IEnumerator - ![w2JMH6f.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/w2JMH6f.png?raw=true) 3. 執行 MoveNext() , 以判斷走訪是否結束. 若尚未結束則將 Current 屬性移動到下一個元素. - ![KuoCeNo.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/KuoCeNo.png?raw=true) 4. 回傳 Current 屬性給 item (也就是 yield return value; 這一行.) - ![1q6yoCx.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/1q6yoCx.png?raw=true) 所以不論是 IEnumerable 或是 IEnumerable<T> 都提供一個 GetEnumerator() 方法. 再透過所得到的 Enumerator 物件去執行走訪這個動作. ### 延遲執行的時機 一般來說程式執行到哪一行 , 該行運算式就應該立即被執行. 但 LINQ 有一個很重要的特性 , 叫做「延遲執行」(deferred execution), 或稱為惰性求值(lazy evaluation). 顧名思義 , 就是在**需要取用查詢結果的時候,才去執行查詢表示式**. 請看下面範例 自定義 Where 以及 Select ```C# // 回傳符合條件的項目 public static IEnumerable<TSource> MyWhere<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) { var iterator = source.GetEnumerator(); while (iterator.MoveNext()) { if (predicate(iterator.Current)) { yield return iterator.Current; } } } ``` ```C# // 將項目轉換成某個樣式 public static IEnumerable<TResult> MySelect<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector) { var iterator = source.GetEnumerator(); while (iterator.MoveNext()) { yield return selector(iterator.Current); } } ``` 測試程式 - query 會篩選 list 中大於 3 的數字並將其加一. ```C# List<int> list = new List<int>() { 5, 9, 8 }; IEnumerable<int> query = list.MyWhere(item => item > 3).MySelect(item => item + 1); list.Remove(9); foreach (var item in query) { Console.WriteLine(item); } ``` 結果會印出 **6 , 9** query 若是立即執行查詢的話 , 則結果應該是 **6 , 10 , 9** 才對. 所以查詢時機應該是在 foreach 那一行. 由此可以推測 LinQ 的延遲執行有兩個特性 - **建立查詢**與**執行查詢**的時機是不同的 - **執行查詢**的時機為存取 IEnumerable 中元素的時候. --- ### 延遲執行的運作過程 #### 測試程式 ```C# public static IEnumerable<TResult> MySelect<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector) { var iterator = source.GetEnumerator(); while (iterator.MoveNext()) { yield return selector(iterator.Current); } } public static IEnumerable<TSource> MyWhere<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) { var iterator = source.GetEnumerator(); while (iterator.MoveNext()) { if (predicate(iterator.Current)) { yield return iterator.Current; } } } public static IEnumerable<(string Name, int Age)> GetStudent() { yield return (Name: "小王", Age: 15); yield return (Name: "大明", Age: 23); yield return (Name: "老黃", Age: 39); } static void Main(string[] args) { var students = GetStudent(); var names = students.MyWhere(student => student.Age > 18).MySelect(student => student.Name); foreach (var name in names) { Console.WriteLine(name); } Console.ReadKey(); } ``` 原本我以為下列這行的執行順序是 Where 計算完結果後 , 在繼續執行 Select 如下圖. ```C# students.MyWhere(student => student.Age>18).MySelect(student => student.Name); ``` ```sequence Note right of IEnumerable: 給予所有學生物件 IEnumerable->Where(Age大於18): Name = "小王", Age = 15 IEnumerable->Where(Age大於18): Name = "大明", Age = 23 IEnumerable->Where(Age大於18): Name = "老黃", Age = 39 Where(Age大於18)->Where(Age大於18): 過濾 : 大於十八歲 Note right of Where(Age大於18): 給予大於十八歲的學生物件 Where(Age大於18)->Select: Name = "大明", Age = 23 Where(Age大於18)->Select: Name = "老黃", Age = 39 Select->Select : 轉換物件為字串 Note right of Select: 給予大於十八歲的學生姓名 Select->輸出結果: Name = "大明" Select->輸出結果: Name = "老黃" ``` 但實際情況卻並非如此 :warning: 再次使用 Visual Studio 去逐步偵錯可發現執行結果為 1. 開始 - ![X296GHF.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/X296GHF.png?raw=true) 2. 不斷按下 F11 , 本以為會進入 GetStudent 內 ,但卻一路執行到 foreach. 原因是 students 以及 names 都是 IEnumerable<T> 型別. 在開始走訪前 , 都不會執行敘述. - ![Y6fbIXl.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/Y6fbIXl.png?raw=true) 3. 呼叫 GetEnumerator() - ![oplSnGE.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/oplSnGE.png?raw=true) 4. 執行 MoveNext() , 這裡指的是 names 的下一個. 但有趣的是 names 的下一個是什麼!? names 其實是從 people.Where().Select() 的結果而來的. 所以要走訪 names 就需要知道 Select() 完的結果是什麼. 因為延遲執行 , 所以 names 的 MoveNext() 會呼叫 Select(). 有點 chain 的感覺. - ![sXr4wdM.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/sXr4wdM.png?raw=true) 5. 進入 Select() , 準備開始走訪. - ![ZoXel4D.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/ZoXel4D.png?raw=true) 6. 當我們在 Select() 方法中 , 呼叫 MoveNext() 時會去執行 Where() 的方法內容 , 因為 source 是 Where()的結果 , 所以想要走訪 source , 就需要取得 Where() 的結果. - ![lNVsYra.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/lNVsYra.png?raw=true) 7. 進入 Where() , 準備開始走訪. - ![bDZxC0t.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/bDZxC0t.png?raw=true) 8. 同理 , where() 內的 source 是 students , 而 studnets 是來自於 GetStudent() 的結果. - ![3OpOWFH.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/3OpOWFH.png?raw=true) 9. 進入 GetStudent() 內 , 並回傳第一個結果 , 小王. - ![fc6euJr.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/fc6euJr.png?raw=true) 10. 回到 Where() , 因為小王不符合 predicate 的條件 , 因此沒進入 if 敘述內. 直接繼續執行 while(). 也就是繼續呼叫 MoveNext(). - ![5DjZzy5.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/5DjZzy5.png?raw=true) 11. 取得第二個結果 , 大明. - ![5GlrB2G.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/5GlrB2G.png?raw=true) 12. 再次回到 Where , 並再次讓 predicate 來判斷. 大明符合條件 , 所以進入 if 區域內執行 yield return , 回傳結果. - ![qyBJiRJ.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/qyBJiRJ.png?raw=true) 13. 回到 Select , 執行 yield retrun , 回傳 selector() 的結果. - ![AD0MsSd.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/AD0MsSd.png?raw=true) 14. 回到 main , name 接收到回傳的結果. - ![5w5x3UT.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/5w5x3UT.png?raw=true) 15. 印出結果**大明** , 之後繼續執行 foreach , 直到 MoveNext() 回傳 false 為止 - ![J1OF8CG.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/J1OF8CG.png?raw=true) 所以實際的執行順序 , 應該如下圖所示 : ```sequence foreach(走訪查詢結果)->Select : 取資料 Select->Where : 取資料 Where->getStudent() : 取資料 getStudent()->Where : 回傳資料 ("小王", 15) Where->Where : Age > 18 ? false Where->getStudent() : 繼續取資料 getStudent()->Where : 回傳資料 ("大明", 23) Where->Where : Age > 18 ? true Where->Select : 回傳資料 ("大明", 23) Select->Select : 經過selector()轉換 Select->foreach(走訪查詢結果) : 回傳 "大明" foreach(走訪查詢結果)->foreach(走訪查詢結果) : 印出 "大明" foreach(走訪查詢結果)->Select : 繼續取資料 , 直到取完. ``` --- ### 結論 1. 可以透過 yield 關鍵字輕易地完成延遲執行的效果. 2. 走訪的動作 , 其實是透過 IEnumerator 來達成. 3. IEnumerable 型別可以作為資料集合操作. 4. 大部分地 LINQ to Objects API 幾乎都是針對IEnumerable<TSource> 進行擴充. 5. 回傳 IEnumerable 型別代表回傳的結果可以走訪. 但卻不會立即走訪. 會直到執行 MoveNext() , 才會開始走訪到下一個. 這也解釋了設定查詢以及執行查詢的驅動時間點不同的原因. 也就是具有延遲執行的特性. 6. 因為 LinQ 具有延遲執行的特性這代表 - 設定查詢式後 , 異動來源資料的內容 , 稍後取得查詢結果時是依據最後的資料集合去做查詢的. - 撰寫一個查詢式後 , 不論要查詢結果幾次 , 都不需要重新撰寫查詢式. 7. 使用 LinQ 時需要注意是否需要立刻取得**即時的**查詢結果. 是否介意查詢結果會隨著查詢源的操作改變而不同. --- ### 補充 : 立即執行 雖然 LinQ 的方法有些具有延遲執行的特性 , 但有些則沒有. 像是 1. 轉型類型的 API - ToList() - ToArray() - ToDictionary() - ToHashSet() - ToLookUp() - ...? 2. 回傳值為單一值 - Max() - Min() - Count() - First() - Last() - Single() - Average() - Sum() - ...? --- ### Thank you! You can find me on - [GitHub](https://github.com/s0920832252) - [Facebook](https://www.facebook.com/fourtune.chen) 若有謬誤 , 煩請告知 , 新手發帖請多包涵 # :100: :muscle: :tada: :sheep: