Try   HackMD

[C#]IEnumerator、IEnumerable、yield

tags: C# Programming

foreach

// 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為什麼知道要做尋訪?

​​​​- 首先,先看看以下程式碼,他並不會編譯成功
int integers = 123456789; foreach (int integer in integers) //error { Console.Write($"{integer} "); }

編譯錯誤為: because 'int' does not contain a public definition for 'GetEnumerator'

看樣子是沒有對int實作GetEnumator,於是把程式改為以下的樣子:

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顯示沒有實作MoveNextmethod與Currentproperty,如果知道設計模式,會知道其中有一個叫做Iterator pattern,以上的錯誤訊息其實就是說我們若要對該資料型別使用foreach進行尋訪,則須對該資料型別實作Iterator pattern

於是再次進化以上的程式:

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#中的IEnumeratorAggreate則是對應到IEnumerable

於是我們在將剛剛的程式進化一次

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原則,把計算邏輯輸出分開,變成了:

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來解決兩個問題:

  • 可以保證計算的花費一定值得(效能問題解決)
  • 可以將巡覽及計算邏輯拆分(可維護性提高)
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

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_yieldfor會持續計算直到找到符合的currentNum,並在此時直接回傳到outputDivide_foreach_List
  2. 輸出之後,再回到enumerable_yield,繼續剛剛的計算,不會再次從頭計算。

yield break

再看看以下程式

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原理說明
仔細體會yield的甜美: yield介紹