迭代器 (Visual Basic)

「迭代器」可用來逐步執行集合,例如清單和陣列。

迭代器方法或 get 存取子會對集合執行自訂反覆運算。 迭代器方法使用 Yield 陳述式,一次傳回一個項目。 當到達 Yield 陳述式時,系統會記住程式碼中的目前位置。 下次呼叫迭代器函式時,便會從這個位置重新開始執行。

您會使用 For Each…Next 陳述式或使用 LINQ 查詢,透過用戶端程式碼取用迭代器。

在下列範例中,第一次反覆運算 For Each 迴圈會使 SomeNumbers 迭代器方法中的執行繼續,直到到達第一個 Yield 陳述式為止。 此反覆運算會傳回值 3,並保留迭代器方法中的目前位置。 下次反覆運算迴圈時,迭代器方法中的執行會從上次停止的位置繼續,並且在到達 Yield 陳述式時再次停止。 此反覆運算會傳回值 5,並再次保留迭代器方法中的目前位置。 當到達迭代器方法結尾時,迴圈便完成。

Sub Main()
    For Each number As Integer In SomeNumbers()
        Console.Write(number & " ")
    Next
    ' Output: 3 5 8
    Console.ReadKey()
End Sub

Private Iterator Function SomeNumbers() As System.Collections.IEnumerable
    Yield 3
    Yield 5
    Yield 8
End Function

迭代器方法或 get 存取子的傳回型別可以是 IEnumerableIEnumerable<T>IEnumeratorIEnumerator<T>

您可以使用 Exit FunctionReturn 陳述式來結束反覆運算。

Visual Basic 迭代器函式或 get 存取子宣告包含 Iterator 修飾元。

迭代器已在 Visual Studio 2012 的 Visual Basic 中引進。

注意

如需本文中除簡易迭代器範例以外的其他所有範例,請包含 System.CollectionsSystem.Collections.Generic 命名空間的 Imports 陳述式。

簡單的 Iterator

下列範例在 For…Next 迴圈內有一行 Yield 陳述式。 在 Main 中,每次反覆運算 For Each 陳述式主體都會建立迭代器函式的呼叫,以繼續進行下一個 Yield 陳述式。

Sub Main()
    For Each number As Integer In EvenSequence(5, 18)
        Console.Write(number & " ")
    Next
    ' Output: 6 8 10 12 14 16 18
    Console.ReadKey()
End Sub

Private Iterator Function EvenSequence(
ByVal firstNumber As Integer, ByVal lastNumber As Integer) _
As System.Collections.Generic.IEnumerable(Of Integer)

    ' Yield even numbers in the range.
    For number As Integer = firstNumber To lastNumber
        If number Mod 2 = 0 Then
            Yield number
        End If
    Next
End Function

建立集合類別

在以下範例中,DaysOfTheWeek 類別會實作 IEnumerable 介面,而這個介面需使用 GetEnumerator 方法。 編譯器會隱含呼叫 GetEnumerator 方法,以傳回 IEnumerator

GetEnumerator 方法會使用 Yield 陳述式一次傳回一個字串,且 Iterator 修飾元會位於函式宣告中。

Sub Main()
    Dim days As New DaysOfTheWeek()
    For Each day As String In days
        Console.Write(day & " ")
    Next
    ' Output: Sun Mon Tue Wed Thu Fri Sat
    Console.ReadKey()
End Sub

Private Class DaysOfTheWeek
    Implements IEnumerable

    Public days =
        New String() {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}

    Public Iterator Function GetEnumerator() As IEnumerator _
        Implements IEnumerable.GetEnumerator

        ' Yield each day of the week.
        For i As Integer = 0 To days.Length - 1
            Yield days(i)
        Next
    End Function
End Class

下列範例會建立 Zoo 類別,其中包含動物的集合。

參考類別執行個體 (theZoo) 的 For Each 陳述式會隱含呼叫 GetEnumerator 方法。 參考 BirdsMammals 屬性的 For Each 陳述式會使用 AnimalsForType 具名迭代器方法。

Sub Main()
    Dim theZoo As New Zoo()

    theZoo.AddMammal("Whale")
    theZoo.AddMammal("Rhinoceros")
    theZoo.AddBird("Penguin")
    theZoo.AddBird("Warbler")

    For Each name As String In theZoo
        Console.Write(name & " ")
    Next
    Console.WriteLine()
    ' Output: Whale Rhinoceros Penguin Warbler

    For Each name As String In theZoo.Birds
        Console.Write(name & " ")
    Next
    Console.WriteLine()
    ' Output: Penguin Warbler

    For Each name As String In theZoo.Mammals
        Console.Write(name & " ")
    Next
    Console.WriteLine()
    ' Output: Whale Rhinoceros

    Console.ReadKey()
End Sub

Public Class Zoo
    Implements IEnumerable

    ' Private members.
    Private animals As New List(Of Animal)

    ' Public methods.
    Public Sub AddMammal(ByVal name As String)
        animals.Add(New Animal With {.Name = name, .Type = Animal.TypeEnum.Mammal})
    End Sub

    Public Sub AddBird(ByVal name As String)
        animals.Add(New Animal With {.Name = name, .Type = Animal.TypeEnum.Bird})
    End Sub

    Public Iterator Function GetEnumerator() As IEnumerator _
        Implements IEnumerable.GetEnumerator

        For Each theAnimal As Animal In animals
            Yield theAnimal.Name
        Next
    End Function

    ' Public members.
    Public ReadOnly Property Mammals As IEnumerable
        Get
            Return AnimalsForType(Animal.TypeEnum.Mammal)
        End Get
    End Property

    Public ReadOnly Property Birds As IEnumerable
        Get
            Return AnimalsForType(Animal.TypeEnum.Bird)
        End Get
    End Property

    ' Private methods.
    Private Iterator Function AnimalsForType( _
    ByVal type As Animal.TypeEnum) As IEnumerable
        For Each theAnimal As Animal In animals
            If (theAnimal.Type = type) Then
                Yield theAnimal.Name
            End If
        Next
    End Function

    ' Private class.
    Private Class Animal
        Public Enum TypeEnum
            Bird
            Mammal
        End Enum

        Public Property Name As String
        Public Property Type As TypeEnum
    End Class
End Class

Try 區塊

Visual Basic 允許在 Try...Catch...Finally StatementTry 區塊中使用 Yield 陳述式。 具有 Yield 陳述式的 Try 區塊可以有 Catch 區塊,也可以有 Finally 區塊。

下列範例會在迭代器函式中包含 TryCatchFinally 區塊。 迭代器函式中的 Finally 區塊會在 For Each 迭代器完成之前執行。

Sub Main()
    For Each number As Integer In Test()
        Console.WriteLine(number)
    Next
    Console.WriteLine("For Each is done.")

    ' Output:
    '  3
    '  4
    '  Something happened. Yields are done.
    '  Finally is called.
    '  For Each is done.
    Console.ReadKey()
End Sub

Private Iterator Function Test() As IEnumerable(Of Integer)
    Try
        Yield 3
        Yield 4
        Throw New Exception("Something happened. Yields are done.")
        Yield 5
        Yield 6
    Catch ex As Exception
        Console.WriteLine(ex.Message)
    Finally
        Console.WriteLine("Finally is called.")
    End Try
End Function

Yield 陳述式不能位於 Catch 區塊或 Finally 區塊中。

如果 For Each 主體 (而非迭代器方法) 擲回例外狀況,則不會執行迭代器函式中的 Catch 區塊,但會執行迭代器函式中的 Finally 區塊。 迭代器函式中的 Catch 區塊僅會攔截迭代器函式中發生的例外狀況。

匿名方法

在 Visual Basic 中,匿名函式可為迭代器函式。 說明如下例。

Dim iterateSequence = Iterator Function() _
                      As IEnumerable(Of Integer)
                          Yield 1
                          Yield 2
                      End Function

For Each number As Integer In iterateSequence()
    Console.Write(number & " ")
Next
' Output: 1 2
Console.ReadKey()

下列範例具有驗證引數的非迭代器方法。 方法會傳回說明集合元素的匿名迭代器結果。

Sub Main()
    For Each number As Integer In GetSequence(5, 10)
        Console.Write(number & " ")
    Next
    ' Output: 5 6 7 8 9 10
    Console.ReadKey()
End Sub

Public Function GetSequence(ByVal low As Integer, ByVal high As Integer) _
As IEnumerable
    ' Validate the arguments.
    If low < 1 Then
        Throw New ArgumentException("low is too low")
    End If
    If high > 140 Then
        Throw New ArgumentException("high is too high")
    End If

    ' Return an anonymous iterator function.
    Dim iterateSequence = Iterator Function() As IEnumerable
                              For index = low To high
                                  Yield index
                              Next
                          End Function
    Return iterateSequence()
End Function

如果驗證位於迭代器函式內,則在開始 For Each 主體的第一個迭代器之前,無法執行驗證。

搭配泛型清單使用迭代器

在以下範例中,Stack(Of T) 泛型類別會實作 IEnumerable<T> 泛型介面。 Push 方法會將值指派給 T 類型的陣列。 GetEnumerator 方法會使用 Yield 陳述式以傳回陣列值。

除了泛型 GetEnumerator 方法,您也必須實作非泛型 GetEnumerator 方法。 這是因為 IEnumerable<T> 繼承自 IEnumerable。 非泛型實作會延後到泛型實作。

此範例使用具名迭代器來支援逐一查看相同資料集合的各種方法。 這些具名迭代器是 TopToBottomBottomToTop 屬性,以及 TopN 方法。

BottomToTop 屬性宣告包含 Iterator 關鍵字。

Sub Main()
    Dim theStack As New Stack(Of Integer)

    ' Add items to the stack.
    For number As Integer = 0 To 9
        theStack.Push(number)
    Next

    ' Retrieve items from the stack.
    ' For Each is allowed because theStack implements
    ' IEnumerable(Of Integer).
    For Each number As Integer In theStack
        Console.Write("{0} ", number)
    Next
    Console.WriteLine()
    ' Output: 9 8 7 6 5 4 3 2 1 0

    ' For Each is allowed, because theStack.TopToBottom
    ' returns IEnumerable(Of Integer).
    For Each number As Integer In theStack.TopToBottom
        Console.Write("{0} ", number)
    Next
    Console.WriteLine()
    ' Output: 9 8 7 6 5 4 3 2 1 0

    For Each number As Integer In theStack.BottomToTop
        Console.Write("{0} ", number)
    Next
    Console.WriteLine()
    ' Output: 0 1 2 3 4 5 6 7 8 9

    For Each number As Integer In theStack.TopN(7)
        Console.Write("{0} ", number)
    Next
    Console.WriteLine()
    ' Output: 9 8 7 6 5 4 3

    Console.ReadKey()
End Sub

Public Class Stack(Of T)
    Implements IEnumerable(Of T)

    Private values As T() = New T(99) {}
    Private top As Integer = 0

    Public Sub Push(ByVal t As T)
        values(top) = t
        top = top + 1
    End Sub

    Public Function Pop() As T
        top = top - 1
        Return values(top)
    End Function

    ' This function implements the GetEnumerator method. It allows
    ' an instance of the class to be used in a For Each statement.
    Public Iterator Function GetEnumerator() As IEnumerator(Of T) _
        Implements IEnumerable(Of T).GetEnumerator

        For index As Integer = top - 1 To 0 Step -1
            Yield values(index)
        Next
    End Function

    Public Iterator Function GetEnumerator1() As IEnumerator _
        Implements IEnumerable.GetEnumerator

        Yield GetEnumerator()
    End Function

    Public ReadOnly Property TopToBottom() As IEnumerable(Of T)
        Get
            Return Me
        End Get
    End Property

    Public ReadOnly Iterator Property BottomToTop As IEnumerable(Of T)
        Get
            For index As Integer = 0 To top - 1
                Yield values(index)
            Next
        End Get
    End Property

    Public Iterator Function TopN(ByVal itemsFromTop As Integer) _
        As IEnumerable(Of T)

        ' Return less than itemsFromTop if necessary.
        Dim startIndex As Integer =
            If(itemsFromTop >= top, 0, top - itemsFromTop)

        For index As Integer = top - 1 To startIndex Step -1
            Yield values(index)
        Next
    End Function
End Class

語法資訊

出現的迭代器可以是方法或 get 存取子。 迭代器不能出現在事件、執行個體建構函式、靜態建構函式或靜態解構函式中。

Yield 陳述式中的運算式類型必須隱含轉換成迭代器的傳回型別。

在 Visual Basic 中,迭代器方法不可有任何 ByRef 參數。

在 Visual Basic 中,"Yield" 不是保留字,只有在 Iterator 方法或 get 存取子中使用時才具有特殊意義。

技術實作

雖然您將迭代器撰寫成方法,但編譯器會將其轉譯成巢狀類別,其實也就是狀態機器。 此類別會在用戶端程式碼中的 For Each...Next 迴圈繼續期間追蹤迭代器的位置。

若要查看編譯器的功能,您可以使用 Ildasm.exe 工具來檢視為迭代器方法產生的通用中間語言程式碼。

當您建立 classstruct 的迭代器時,您不需要實作整個 IEnumerator 介面。 當編譯器偵測到迭代器時,它會自動產生 IEnumeratorIEnumerator<T> 介面的 CurrentMoveNextDispose 方法。

之後每次反覆運算 For Each…Next 迴圈 (或直接呼叫 IEnumerator.MoveNext),下一個迭代器程式碼主體都會在上一個 Yield 陳述式之後繼續。 然後繼續執行至下一個 Yield 陳述式,直到達到迭代器主體結尾,或遇到 Exit FunctionReturn 陳述式為止。

迭代器不支援 IEnumerator.Reset 方法。 若要從頭開始逐一查看,您必須取得新的迭代器。

如需其他資訊,請參閱 Visual Basic 語言規格

迭代器的使用

當您需要使用複雜的程式碼來填入清單序列時,迭代器可讓您維持 For Each 迴圈的簡潔性。 當您想要執行下列作業時,這會很有用:

  • 在第一次反覆運算 For Each 迴圈之後修改清單序列。

  • 避免在第一次反覆運算 For Each 迴圈之前完整載入大型清單。 分頁擷取以分批載入資料表資料列即為一例。 另一個範例是 EnumerateFiles 方法,它會在 .NET Framework 中實作迭代器。

  • 在迭代器中封裝建立清單。 在迭代器方法中,您可以建立清單,然後在迴圈中產生每個結果。

另請參閱