[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 --- ![](https://i.imgur.com/uYzPJPe.png) - **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)