---
# System prepended metadata

title: '[筆記] 從 IEnumerable 介面 到 Yield 關鍵字'
tags: [IL, IEnumerable, IEnumerator, Foreach, Yield]

---

# 從 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 物件
        ![image](https://hackmd.io/_uploads/SksYQWCG0.png)
    - **方法二**：採用 foreach 關鍵字
        ![image](https://hackmd.io/_uploads/ryD2mWCzR.png)
    - **發現**：方法二除了 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/>
:::