Visual Basic 11 的 Iterator 與 Yield 心得筆記(1)

在前一篇「Visual Basic 11 Beta 新功能介紹」我有提到一個 Iterator 與 Yield,這個 Iterator 與 Yield 是 C# 2.0 (Visual Studio 2005)提供的功能,它提供了簡易實作出【反覆運算】函式的功能,換成白話一點的說法,您想想,如果您想讓您的類別或函式能讓 For Each 使用,您應該怎麼做?

說來簡單,您只需實作整個 IEnumerable 介面IEnumerable(Of T) 介面。我們以MSDN中 IEnumerable 介面 範例來說明 IEnumerable 介面實作。

Imports System
Imports System.Collections

''' <summary>
''' Person 類例
''' </summary>
Public Class Person

    Public firstName As String
    Public lastName As String

    ''' <summary>
    ''' 建構式
    ''' </summary>
    ''' <param name="fName">姓</param>
    ''' <param name="lName">名子</param>
    Sub New(ByVal fName As String, ByVal lName As String)
        Me.firstName = fName
        Me.lastName = lName
    End Sub
End Class

首先,宣告一個Class Person(個人),裡面只有兩個屬性,firstName 和 lastName,並且了一個簡單的初始化建構式。

''' <summary>
''' People 類別
''' </summary>
''' <remarks>實作 IEnumerable</remarks>
Public Class People
    Implements IEnumerable

    ' 私有 _person()陣列變數,存放 Person類別資料
    Private _people() As Person

    ''' <summary>
    ''' 建構式
    ''' </summary>
    ''' <param name="pArray">Person 陣列</param>
    Sub New(pArray() As Person)
        ' 初始化陣列物件
        _people = New Person(pArray.Length - 1) {}
        
        ' 儲存至私有 _person()陣列變數
        Dim i As Integer
        For i = 0 To pArray.Length - 1
            ' 將傳入物件陣列儲存在 _people陣列
            _people(i) = pArray(i)
        Next i
    End Sub

    ''' <summary>
    ''' 實作 IEnumerable.GetEnumerator 方法
    ''' </summary>
    ''' <returns>列舉 _people()陣列的結果</returns>
    Function GetEnumerator() As IEnumerator _
                     Implements IEnumerable.GetEnumerator

        ' 回傳列舉 _people陣列結果
        Return New PeopleEnum(_people)
    End Function
End Class

宣告一個Class People(人),重點在第二行,它實作了 IEnumerable介面,當您輸入完 Implements IEnumerable 按下 Enter 後,會自動跳出需實作的函式,即 GetEnumerator() 函式。People類別主要是要存放 Person(個人)類別資料,存放於陣列之中,然實透過實作 GetEnumerator() 函式,讓 People類別擁有可反覆運作的能力。在 GetEnumerator() 函式中,我們還需要透過一個 PeopleEnum類別來幫忙完成最後一件事。

''' <summary>
''' 列舉 People 類別
''' </summary>
''' <remarks>實作 IEnumerable</remarks>
Class PeopleEnum
    Implements IEnumerator

    Public _people() As Person

    ' 在第一個列舉之前的位置
    ' 直到第一次呼叫 MoveNext()
    Dim position As Integer = -1

    ''' <summary>
    ''' 建構式,初始化 _people() 陣列
    ''' </summary>
    ''' <param name="list">Person陣列</param>
    Sub New(list() As Person)
        _people = list
    End Sub

    ''' <summary>
    ''' 實作 MoveNext() 函式
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Function MoveNext() As Boolean _
                    Implements IEnumerator.MoveNext

        position = position + 1
        Return (position < _people.Length)
    End Function

    ''' <summary>
    ''' 實作 Reset() 副程式
    ''' </summary>
    Sub Reset() Implements IEnumerator.Reset
        position = -1
    End Sub

    ''' <summary>
    ''' 實作唯讀 Current() 屬性
    ''' </summary>
    ''' <value></value>
    ''' <returns>_people陣列值</returns>
    ''' <remarks></remarks>
    Public ReadOnly Property Current() As Object _
                    Implements IEnumerator.Current
        Get
            Try
                ' 回傳現在位置的 _people陣列值
                Return _people(position)
            Catch ex As IndexOutOfRangeException
                Throw New InvalidOperationException()
            End Try
        End Get
    End Property
End Class

PeopleEnum類別,很明顯是在進行列舉用的類別。


注意實作的是 IEnumerator介面,我們必須實作此 IEnumerator介面裡的三個方法。首先是 position 變數,For Each 每次呼叫時的動作,其實都是呼叫 MoveNext() 函式,陣列由 0 開始,所以第一次呼叫的值應該是如 _people(0) 來回傳第一筆陣列值,所以我們一開始先讓 position 的值為 -1。在 MoveNext() 函式中,我們做的事很簡單,一是先進行 +1 的動作,然後判斷 +1 之後是否超出陣列大小。在 Reset() 更簡單,Reset 代表重新設定,就讓 position 回到最原頭的 -1。最後一個也是最重要的屬性,Current() 屬性,也就是回傳當前陣列的值。

最後整個完成的類別圖:

圖一:實作 IEnumerable 介面範例類別圖
People類別主要包含 Person資料,但透過 PeopleEnum類別來提供列舉的能力。

最後執行的程式碼。

Module Module1

    Sub Main()
        Dim peopleArray() As Person = {
            New Person("John", "Smith"),
            New Person("Jim", "Johnson"),
            New Person("Sue", "Rabon")}

        Dim peopleList As New People(peopleArray)
        Dim p As Person

        ' 注意,peopleList 可以使用 For Each 列舉
        For Each p In peopleList
            Console.WriteLine(p.firstName + " " + p.lastName)
        Next

        Console.ReadLine()

        ' 結果:
        ' 
        ' John Smith
        ' Jim Johnson
        ' Sue Rabon
    End Sub
End Module

我們傳入一個 Person的陣列物件(peopleArray)給 People類別的執行個體 peopleList。 peopleList 執行個體為什麼能有列舉能力,因為 People類別有實作 IEnumerable介面。那您會說,我平常在用的 Array、ArrayList、List … 不都可以直接使用嗎?對,你去查查 MSDN,那是因為人家在這些類別裡已經幫您實作好 IEnumerable介面。我們在來看一個簡單的範例,一週的日期。

Module Module1
    Sub Main()
        ' 參考 ArrayList 的宣告
        ' Public Class ArrayList
        '         Implements IList, ICollection, IEnumerable, ICloneable
        Dim weekDayList As New ArrayList From
            {"Sun-星期日", "Mon-星期一", "Tue-星期二",
             "Wed-星期三", "Thu-星期四", "Fri-星期五", "Sat-星期六"}

        ' 找出 S 開頭的日期
        Console.WriteLine("使用 For Each 找出 S 開頭的日期")
        Dim day As String = ""
        For Each day In weekDayList
            If day.StartsWith("S") Then
                Console.WriteLine(day.ToString())
            End If
        Next
        Console.WriteLine()
        Console.ReadLine()

        Console.WriteLine("使用 IEnumerable介面來找出 T 開題的日期")

        ' 讓 dayEnum 擁有列舉能力
        Dim dayEnum As IEnumerator = weekDayList.GetEnumerator()
        While dayEnum.MoveNext
            Dim dayString As String = dayEnum.Current.ToString()
            If dayString.StartsWith("T") Then
                Console.WriteLine(dayString)
            End If
        End While
        Console.WriteLine()
        Console.ReadLine()

        ' 結果
        ' 使用 For Each 找出 S 開頭的日期
        ' Sun-星期日
        ' Sat-星期六
        '
        ' 使用 IEnumerable介面來找出 T 開題的日期
        ' Tue-星期二
        ' Thu-星期四
    End Sub
End Module

ArrayList類別是一個已經實作 IEnumerable介面的類別,所以我們很輕易在程式中使用 For Each 來取得資料。第二段程式主要是透過 GetEnumerator() 來讓 dayEnum 有列舉能力(IEnumerator介面提供),然後透過 MoveNext 與 Current 來移動陣列與取出陣列值。

看完第一段(Person)與第二段(dayEnum)程式碼,有沒有發現,人家幫我們做了好多事。但如一開頭所言,一句「實作 IEnumerable介面」,你看看要做多少事?

沒有留言:

張貼留言

感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。