前往
大廳
主題

[Design Pattern C#] Iterator Pattern 迭代器模式 與 yield

帥氣跳蚤蛋 | 2021-10-02 21:30:02 | 巴幣 12 | 人氣 501

關於在迭代器模式的實作上,C#已提供了很方便的解決方案,
只要實作IEnumerator與IEnumerable兩個介面,即可實現此模式,
若使用C#的語法糖"yield",程式會變得更簡潔.
本文會對IEnumerator、IEnumerable還有yield進行範例的講解.
===========================================================================
迭代器模式通常都會搭配foreach使用,
若該資料結構要使用foreach,需繼承自IEnumerable,
我們常用String、Array與List,可使用foreach取出內部的資料,
是因為都有實作IEnumerable這個介面.

List<int> nums = new List<int> { 1, 2, 3, 4, 5 }; //建立List
foreach (int i in nums) //使用foreach取出所有的數值
{
    Console.WriteLine(i);
}
/*result:
* 1
* 2
* 3
* 4
* 5
*/
===========================================================================
C#在實現迭代器模式時,必要實作IEnumerator與IEnumerable這兩個介面
foreach會呼叫IEnumerator介面中的GetEnumerator()
此方法回傳IEnumerator型別,
而IEnumerator包含了MoveNext(),Reset()與Current三個方法與屬性
實作這三個方法與屬性即可讓foreach正常工作,
實作IEnumerator的類別,使用foreach不僅能遍歷資料,也可依據需求,輸出特定條件的資料.

本範例會利用foreach,列印出Student類別的陣列中大於18歲的資料.

首先建立Student類別,後面範例會使用foreach來找尋此類別的資料.
public class Student
{
    public int Age { get; set; }
    public string Name { get; set; }
    public int Student_ID { get; set; }
    public override string ToString()   //複寫ToString方便最後給Console.WriteLine()呈現資料
    {
        return $"name= {Name} age= {Age} Student_ID= {Student_ID,0:D3}";
    }
}

建立ClassStudentEnum類別,此類別要實作IEnumerator,
此類別會被IEnumerable的GetEnumerator()進行調用,
在IEnumerator有三個成員需要實作,皆由foreach調用:
1. Current: 返回搜尋的結果,本範例為返回Student類別
2. MoveNext(): 返回bool,確認是否還有資料需要輸出
3. Reset(): 設定列舉的初始位置
由於本範例是使用泛型的IEnumerator,
故要實作Dispose(),此方法用於釋放資源.

在本範例中只要顯示18歲以上學生的資料,在MoveNext()中篩選資料
只有在找到大於18歲以上時,才會返回True,
讓Current可取得正確的資料.
public class ClassStudentEnum : IEnumerator<Student>
{
    private Student[] _students;
    private int index=-1;

    public ClassStudentEnum(Student[] students)
    {
        _students = students;   //取得Student[]
    }

    public Student Current  //返回搜尋的結果
    {
        get
        {
            return _students[index];
        }
    }

    object IEnumerator.Current => Current;

    public void Dispose()   //釋放資源
    {
        _students = null;
    }

    public bool MoveNext()  //確認是否還有資料需要輸出
    {
        while (index < _students.Length-1)
        {
            index++;
            if (_students[index].Age >= 18) //輸出大於18歲以上的學生資料
                return true;
        }
        return false;
    }

    public void Reset() //設定列舉的初始位置
    {
        index = -1;
    }
}

再來建立ClassStudent實作IEnumerable,
讓foreach可透過GetEnumerator()取得實作的IEnumerator.
public class ClassStudent : IEnumerable<Student>
{
    private ClassStudentEnum _classStudentEnum;

    public ClassStudent(Student[] students)
    {
        _classStudentEnum = new ClassStudentEnum(students); //產生IEnumerator的物件
    }

    public IEnumerator<Student> GetEnumerator() //此方法會被foreach調用,返回IEnumerator
    {
        return _classStudentEnum;
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

最後在Main建立Student的陣列,並建立列舉,
使用foreach進行調用,即可得到結果,
本範例由foreach顯示出Student陣列中,
大於18歲以上,學生的資料.
static void Main(string[] args)
{
    Student[] students = new Student[]  //建立student的陣列
    {
                new Student{Age=15,Name="Ben",Student_ID=001},
                new Student{Age=20,Name="Danny",Student_ID=002},
                new Student{Age=25,Name="Lucia",Student_ID=003},
                new Student{Age=18,Name="Daisy",Student_ID=004},
                new Student{Age=12,Name="Rob",Student_ID=005},
                new Student{Age=17,Name="Teddy",Student_ID=006},
                new Student{Age=22,Name="Jill",Student_ID=007}
    };

    ClassStudent classStudent = new ClassStudent(students); //將students放入列舉

    foreach (Student student in classStudent)   //foreach調用IEnumerable與IEnumerator
    {
        Console.WriteLine(student);
    }
    /* result:
     * name= Danny age= 20 Student_ID= 002
     * name= Lucia age= 25 Student_ID= 003
     * name= Daisy age= 18 Student_ID= 004
     * name= Jill age= 22 Student_ID= 007
     */
}
===========================================================================
接下來,示範另一個C#的語法糖yield,
這顆糖啊~讓迭代器模式的實作又更簡單了,

本節仍使用上面的範例,並用yield進行實作
搜尋Student類別的陣列,顯示大於18歲學生的資料,

建立靜態方法IteratorMethod(),返回值為IEnumerable,
搜尋Student陣列的資料,
當學生大於18歲,利用yield return返回資料
public class Student_yield
{
    public static IEnumerable<Student> IteratorMethod(Student[] students)   //回傳IEnumerable,並實作yield return
    {
        for (int i = 0; i < students.Length; i++)
        {
            if (students[i].Age >= 18)  //輸出大於18歲的學生資料
                yield return students[i];
        }
    }
}

然後在Main一樣建立Student的陣列,
就可利用foreach返回Student陣列中,大於18歲的學生資料.

這就是yield的魅力啦~整個程式碼短很多了吧
static void Main(string[] args)
{
    Student[] students = new Student[]  //建立student的陣列
    {
                new Student{Age=15,Name="Ben",Student_ID=001},
                new Student{Age=20,Name="Danny",Student_ID=002},
                new Student{Age=25,Name="Lucia",Student_ID=003},
                new Student{Age=18,Name="Daisy",Student_ID=004},
                new Student{Age=12,Name="Rob",Student_ID=005},
                new Student{Age=17,Name="Teddy",Student_ID=006},
                new Student{Age=22,Name="Jill",Student_ID=007}
    };

    foreach (Student student in Student_yield.IteratorMethod(students))  //由foreach調用Student_yield.IteratorMethod()
    {
        Console.WriteLine(student); //顯示yield return返回的資料
    }
    
    /* Result:
     * name= Danny age= 20 Student_ID= 002
     * name= Lucia age= 25 Student_ID= 003
     * name= Daisy age= 18 Student_ID= 004
     * name= Jill age= 22 Student_ID= 007
     */
}

關於yield本篇文章僅講解部分功能,
yield不僅能傳回IEnumerable,也能進行非同步的調用,
關於yield的其他功能,可參考yield的官方說明頁面: yield (C# 參考)
===========================================================================

有完整的程式碼,包含2個專案:
C Sharp Iterator Pattern Exercise: 使用IEnumerable與IEnumerator實現
C Sharp yield Exercise: 使用yield實現
===========================================================================
本文轉載自本帥的Medium部落格: 跳蚤的蛋蛋跳蚤蛋
送禮物贊助創作者 !
0
留言

創作回應

oVo巴爾坦星人
剛訂閱了XD
2021-11-15 17:23:21
帥氣跳蚤蛋
我也還在努力學習中,歡迎互相交流
2021-11-15 18:41:47

更多創作