# 從 IEnumerable 介面 到 Yield 關鍵字 從C#中,**IEnumerable** 和 **IEnumerator** 是滿重要的概念,本文從 **IEnumerable** 和 **IEnumerator** 的 Why How What 談起,再進到 **Yield** 關鍵字,以及使用 **Yield** 對於效能上面的影響 # What are they? - 類別意義上:**I** 代表「介面」,因此 IEnumerable 和 IEnumerator 都是一種介面 - 翻譯意義上:撇除「介面」不看,Enumerate 代表 *to name things separately, one by one*,中文上可稱為「列舉」或「枚舉」,因此,Enumerable 就是「可列舉的」;Enumerator 就是「列舉器」 - 程式碼意義上:實作 IEnumerable 和 IEnumerator 介面,就可以使用「**foreach**」語法 ```csharp public interface IEnumerable { IEnumerator GetEnumerator(); } public interface IEnumerator { object Current { get; } bool MoveNext; void Reset(); } ``` # How it works? 我們嘗試建立兩個類別,分別實作 IEnumerable 和 IEnumerator 介面 - 建立一個 PeopleEnumerator 類別,他實作 IEnumerator 介面 ```csharp= public class PeopleEnumerator : IEnumerator { private int _start; private int _end; private int _current; public PeopleEnumerator(int start, int end) { _start=start; _end=end; _current = start; } public object Current { get { return _current; } } public bool MoveNext() { _current++; if (_current > _end) { return false; } else return true; } public void Reset() { _start = 0; } } ``` - 建立一個 People 類別,他實作 IEnumerable 介面 ```csharp= public class People : IEnumerable { public IEnumerator GetEnumerator() { return new PeopleEnumerator(0, 100); } } ``` - 在 Console App 的 Main Method 中,分別運行 **分法一** 和 **方法二** ```csharp= static void Main(string[] args) // 方法一:採用 peopleEnumerator 物件 { People people = new People(); IEnumerator peopleEnumerator = people.GetEnumerator(); while (peopleEnumerator.MoveNext()) { Console.WriteLine(peopleEnumerator.Current); } } static void Main(string[] args) // 方法二:採用 foreach 關鍵字 { People people = new People(); foreach (var item in people) { Console.WriteLine(item); } } ``` - 比較 **分法一** 和 **方法二** 在 Intermediate Language (IL) 中的差異 - **方法一**:採用 peopleEnumerator 物件  - **方法二**:採用 foreach 關鍵字  - **發現**:方法二除了 item 變數 和 Dispose 處理外,兩者幾乎一致! # Why we need it? - 透過實作 IEnumerable 和 IEnumerator 介面,我們可以讓**特定類別進行 foreach** # **IEnumerable** 和 **IEnumerator** 的小節: 1. 於**方法一**中,我們使用建立了 peopleEnumerator 物件,若沒有進到 while 迴圈,它僅僅是一個物件;而進到迴圈之後,每次也僅是對於一個 _current 進行處理和回傳,其後,_current 就會被覆蓋,此有助於改善電腦的記憶體效能 2. 我們並不用透過**方法一**的方式建立 peopleEnumerator 物件,我們可以採用**方法二**的 foreach 關鍵字進行 code 的簡化,兩者執行上是等價的 # What is "Yield"? - 翻譯意義上:是 *得到、產出* 的意思,或許可以解讀成,*得到、產出* 一個 return,但還有下一個 return - 程式碼意義上:是一個關鍵字,搭配 return 一併使用。當進行 foreach 時候,將會反覆查詢下一個 **yield return** # How to use "Yield"? - 我們建立一個 Car 類別,內部沒有實作任何介面,但以下兩種方法,可以透過 yield return 不同的值 - 方法一:會回傳 IEnumerable 物件 - 方法二:會回傳 IEnumerator 物件 ```csharp= public class Car { public IEnumerable GetEnumerable() { yield return 1; yield return 2; yield return 3; } public IEnumerator GetEnumerator() { yield return 4; yield return 5; yield return 6; } } ``` - 於 Console App 的 Main 方法中,建立 cars 物件,分別呼叫兩種方法 ```csharp= void Main() { Car cars=new Car(); foreach (var car in cars.GetEnumerable()) // 呼叫 GetEnumerable 方法 { Console.WriteLine(car); // print 1 2 3 } foreach (var car in cars) // 呼叫 GetEnumerator 方法 { Console.WriteLine(car); // print 4 5 6 } Console.ReadLine(); } ``` - **發現**: - 使用 yield return 回傳 IEnumerable 物件,將需要該 cars 物件取的 IEnumerable 物件(即呼叫 GetEnumerable()方法),接這就可以透過 foreach 進行 car 的 列舉 - 使用 yield return 回傳 IEnumerator 物件,該方法名稱一定要是 GetEnumerator,而 cars 物件不需要呼叫任何方法,就可以透過 foreach 進行 car 的 列舉 - 兩者再 Intermediate Language(IL) 中都是建立了虛擬的 GetEnumerable 和 GetEnumerator 類別資料,換句話說,就是 IL 已經幫我們寫好 Code 了,AKA, yield 就是語法糖(Syntax Sugar) - 使用 yield return ,可以減少介面實作的程式碼實作,透過單一方法就可以讓特定類別進行 foreach # Why to use "Yield"? - 首先,作為一個語法糖,使用 Yield 可以減少程式碼撰寫(不用再寫一個類別實作 IEnumerable 或者 IEnumerator) - 接著,以下例子展現,使用 Yield 可以減少資料處理時間 ```csharp= void Main() { List<int> ListofNum=new List<int>(); for(int i=0;i<100000;i++) { ListofNum.Add(i); } // 方案一:使用 foreach + 印出 item Stopwatch stopwatch_ListLoop = new Stopwatch(); stopwatch_ListLoop.Start(); NotUseYield(ListofNum); stopwatch_ListLoop.Stop(); Console.WriteLine("List Loop Time:"+stopwatch_ListLoop.Elapsed); // 測試方案二時,以上進行註解 // List Loop Time:00:00:00.4277934 // 方案二:foreach + Yield Return Stopwatch stopwatch_YieldLoop = new Stopwatch(); stopwatch_YieldLoop.Start(); Console.WriteLine(UseYieldReturn(ListofNum)); stopwatch_YieldLoop.Stop(); Console.WriteLine("Yield Loop:"+stopwatch_YieldLoop.Elapsed); // 測試方案一時,以上進行註解 // Yield Loop:00:00:00.0023577 Console.ReadLine(); } // 搭配方案一:foreach + 印出 item 方法 public void NotUseYield(List<int> input) { foreach(var item in input) { if(item%2==0) { Console.WriteLine(item); } } } // 搭配方案二:foreach + Yield Return 方法 public IEnumerable UseYieldReturn(List<int> input) { foreach(var item in input) { if(item%2==0) { yield return item; } } } ``` - 由上面例子,我們可以發現,透過 Yield 建立篩選的邏輯,可以加快資料的處理速度,而當資料量越大時,Yield 越能顯現其效益 # 結論: 1. 使用 IEnumerable 和 IEnumerator,可以讓特定類別使用 Foreach 關鍵字 2. Foreach 關鍵字背後的實作原理是利用 IEnumerable 和 IEnumerator 搭配 While 迴圈進行處理 3. Yield 關鍵字為一種語法糖,編譯時期會進行建立相關類別,以簡化 IEnumerable 和 IEnumerator 的實作 4. 使用 Yield Return 可以加快資料的處理速度 --- :::info :bookmark_tabs: **參考資料** >MS 官網 Iterator:<https://learn.microsoft.com/zh-tw/dotnet/csharp/programming-guide/concepts/iterators> >黑暗執行緒 Blog:<https://blog.darkthread.net/blog/yield-return/> :::
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up