[C#]IEnumerator、IEnumerable、yield
===
###### tags: `C#` `Programming`
foreach
---
```C#=
// Print integer array
int[] integers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Console.WriteLine($"Is Array: {integers is Array}"); //Is Array: true
foreach (int integer in integers)
{
Console.Write($"{integer} ");
} // 1 2 3 4 5 6 7 8 9
// Print string word by word
String integers = "123456789";
Console.WriteLine($"Is Array: {integers is Array}"); //Is Array: false
foreach (char integer in integers)
{
Console.Write($"{integer} ");
} // 1 2 3 4 5 6 7 8 9
```
以上的程式碼,比起使用`for`做尋訪,`foreach`看起來更為整潔,而**foreach為什麼知道要做尋訪**?以及**與for迴圈相比需要有甚麼代價?**
### foreach為什麼知道要做尋訪?
- 首先,先看看以下程式碼,他並不會編譯成功
```C#=
int integers = 123456789;
foreach (int integer in integers) //error
{
Console.Write($"{integer} ");
}
```
==**編譯錯誤為:** because 'int' does not contain a public definition for 'GetEnumerator'==
看樣子是沒有對`int`實作`GetEnumator`,於是把程式改為以下的樣子:
```C#=
public class IntegerEnum
{
}
public class Integers
{
private int _integers; // 儲存一個int
public Integers(int integers)
{
_integers = integers;
}
public IntegerEnum GetEnumerator()
{
return new IntegerEnum();
}
}
Integers integers = new Integers(123456789);
foreach(var integer in integers)
{
Console.WriteLine($"{integer} ");
}
```
==**還是一樣出現了錯誤:** Integers.GetEnumerator()' must have a suitable public MoveNext method and public Current property==
- Log顯示沒有實作`MoveNext`method與`Current`property,如果知道設計模式,會知道其中有一個叫做`Iterator pattern`,以上的錯誤訊息其實就是說我們**若要對該資料型別使用foreach進行尋訪,則須對該資料型別實作Iterator pattern**。
於是再次進化以上的程式:
```C#=
public class IntegerEnum
{
private int _integers;
private int _index;
private int _maxDigit;
public int Current { get; private set; }
public IntegerEnum(int integers)
{
_integers = integers;
_index = 0;
_maxDigit = (int)Math.Log10(integers);
}
public bool MoveNext()
{
if (_maxDigit < _index) return false;
Current = getCurrent();
_index++;
return true;
}
private int getCurrent()
{
int currentDigit = _maxDigit - _index;
int result = (_integers / (int)Math.Pow(10, currentDigit)) % 10; //Get first digit
return result;
}
}
public class Integers
{
private int _integers;
public Integers(int integers)
{
_integers = integers;
}
public IntegerEnum GetEnumerator()
{
return new IntegerEnum(_integers);
}
}
Integers integers = new Integers(123456789);
foreach(var integer in integers)
{
Console.WriteLine($"{integer} ");
}
// Output: 1 2 3 4 5 6 7 8 9
```
Iterator pattern
---

- **ConcreteAggregate:** `Integers`
Iterator(): `GetEnumerator()`
- **ConcreteIterator:** `IntegerEnum`
`next()`: 傳回下一個元素,在C#中是以`Current`來抓出目前元素
`hasNext()`: 確認是否有下一個元素,在C#中是由`MoveNext()`做確認
- 在Iterator裡有個跟C#上的實作差異
- 在Iterator Pattern上是用hasNext()判斷是否有下一個元素,確定有了再Call Next()取得元素並更新index C#裡是用MoveNext()判斷是否有下一個元素,確定有了之後去更新Current及index
IEenumerable、IEnumerator
---
C# 已經有可以拿來實作Iterator pattern的interface,以上那張圖的`Iterator`對應的是C#中的`IEnumerator`,`Aggreate`則是對應到`IEnumerable`。
於是我們在將剛剛的程式進化一次
```C#=
public class IntegerEnum : IEnumerator
{
public object Current { get; private set; }
private int _integers;
private int _index;
private int _maxDigit;
public IntegerEnum(int integers)
{
_integers = integers;
_index = 0;
_maxDigit = (int)Math.Log10(integers);
}
public bool MoveNext()
{
if (_maxDigit < _index)
{
return false;
}
Current = GetCurrent();
_index++;
return true;
}
public void Reset()
{
_index = 0;
}
private int GetCurrent()
{
int currentDigit = _maxDigit - _index;
int result = (_integers / (int)Math.Pow(10, currentDigit)) % 10;
return result;
}
}
public class Integer : IEnumerable
{
private int _integers;
public Integer(int integers)
{
_integers = integers;
}
public IEnumerator GetEnumerator()
{
return new IntegerEnum(_integers);
}
}
Integer integers = new Integer(123456789);
foreach (var i in integers)
{
Console.Write($"{i} ");
}
```
yield
---
- **情境題**: 在1到特定數字中輸出可以被某數整除的數列
為了實現SRP原則,把**計算邏輯**與**輸出**分開,變成了:
```C#=
private static void outputDivide_foreach_List(int maxNum, int divide)
{
foreach (int item in enumerable_List(maxNum, divide))
{
Console.Write($"{item} ");
}
Console.WriteLine();
}
private static IEnumerable enumerable_List(int maxNum, int divide)
{
List<int> result = new List<int>();
for (int currentNum = 1; currentNum <= maxNum; currentNum++)
{
if (currentNum % divide != 0) continue;
result.Add(currentNum);
}
return result;
}
```
- 在這裡有些隱憂:
- 在`enumerable_List`中的`for`迴圈中,不管有沒有找到符合的數字,一定會把整個範圍跑一遍,再把數字存入`List`
- `List`會有額外的空間開銷
實作Iterator pattern來解決兩個問題:
- 可以保證計算的花費一定值得(效能問題解決)
- 可以將巡覽及計算邏輯拆分(可維護性提高)
```C#=
private static IEnumerable enumerable_Iterator(int maxNum, int divide)
{
integersAggregate enumerable = new integersAggregate(maxNum, divide);
return enumerable;
}
private class integersAggregate : IEnumerable
{
private int _maxNum;
private int _divide;
public integersAggregate(int maxNum, int divide)
{
_maxNum = maxNum;
_divide = divide;
}
public IEnumerator GetEnumerator()
{
return new integersInterator(_maxNum, _divide);
}
}
private class integersInterator : IEnumerator
{
private int _maxNum;
private int _divide;
private int currentNum = 1;
public integersInterator(int maxNum, int divide)
{
_maxNum = maxNum;
_divide = divide;
}
public object Current { get; private set; }
public bool MoveNext()
{
do
{
if (currentNum % _divide == 0)
{
Current = currentNum;
return true;
}
currentNum++;
} while (currentNum <= _maxNum);
return false;
}
public void Reset()
{
currentNum = 1;
}
}
```
***但人就是懶,一定要實作Iterator pattern嗎?答案是不用,我們可以用C#的語法糖 `yield`***
```C#=
private static void outputDivide_foreach_List(int maxNum, int divide)
{
foreach (int item in enumerable_List(maxNum, divide))
{
Console.Write($"{item} ");
}
Console.WriteLine();
}
private static IEnumerable enumerable_yield(int maxNum, int divide)
{
for (int currentNum = 1; currentNum <= maxNum; currentNum++)
{
if (currentNum % divide != 0) continue;
yield return currentNum;
}
}
```
該方法會怎麼運作?
1. 在`outputDivide_foreach_List`呼叫`enumerable_yield`時,`enumerable_yield`的`for`會持續計算直到找到符合的`currentNum`,並在此時直接回傳到`outputDivide_foreach_List`
2. 輸出之後,再回到`enumerable_yield`,繼續剛剛的計算,不會再次從頭計算。
### yield break
再看看以下程式
```C#=
private static IEnumerable enumerable_yield2()
{
yield return 1;
yield return 2;
yield return 3;
yield return 4;
yield return 5;
yield return 6;
yield return 7;
yield return 8;
yield return 9;
yield break;
yield return 10;
//1 2 3 4 5 6 7 8 9
}
```
這支方法在運行到`yield break`時,就會直接離開,並不會在回傳`10`
參考資料
---
[藏在foreach下的秘密: foreach原理說明](https://ithelp.ithome.com.tw/articles/10193261)
[仔細體會yield的甜美: yield介紹](https://ithelp.ithome.com.tw/articles/10193586)