For Each...Next 语句 (Visual Basic)

为集合中每个元素重复一组语句。

语法

For Each element [ As datatype ] In group
    [ statements ]
    [ Continue For ]
    [ statements ]
    [ Exit For ]
    [ statements ]
Next [ element ]

组成部分

术语 定义
element For Each 语句中是必需的。 在 Next 语句中是可选的。 变量。 用于循环访问集合中的元素。
datatype 如果 Option Infer 为 on(默认值)或 element 已声明,则是可选的;如果 Option Infer 为 off 且 element 尚未声明,则是必需的。 element 的数据类型。
group 必需。 类型是集合类型或对象的变量。 引用要对其重复 statements 的集合。
statements 可选。 For EachNext 之间的一个或多个语句,会对 group 中的每个项运行。
Continue For 可选。 将控制转移到 For Each 循环的开头。
Exit For 可选。 将控制转移到 For Each 循环外。
Next 必需。 终止 For Each 循环的定义。

简单示例

要为集合或数组的每个元素重复一组语句时,可使用 For Each...Next 循环。

提示

在可以将循环的每次迭代与控制变量关联并确定该变量的初始值和最终值时,For...Next 语句效果会很好。 但是在处理集合时,初始值和最终值的概念没有意义,你并不一定了解集合有多少个元素。 在这种情况下,For Each...Next 循环通常是更好的选择。

在下面的示例中,For EachNext 语句会循环访问列表集合的所有元素。

' Create a list of strings by using a
' collection initializer.
Dim lst As New List(Of String) _
    From {"abc", "def", "ghi"}

' Iterate through the list.
For Each item As String In lst
    Debug.Write(item & " ")
Next
Debug.WriteLine("")
'Output: abc def ghi

有关更多示例,请参阅集合数组

Nested Loops

可以通过将 1 个循环放入另一个循环来嵌套 For Each 循环。

下面的示例演示了嵌套 For EachNext 结构。

' Create lists of numbers and letters
' by using array initializers.
Dim numbers() As Integer = {1, 4, 7}
Dim letters() As String = {"a", "b", "c"}

' Iterate through the list by using nested loops.
For Each number As Integer In numbers
    For Each letter As String In letters
        Debug.Write(number.ToString & letter & " ")
    Next
Next
Debug.WriteLine("")
'Output: 1a 1b 1c 4a 4b 4c 7a 7b 7c

嵌套循环时,每个循环必须具有唯一的 element 变量。

还可以在彼此之间嵌套不同种类的控制结构。 有关详细信息,请参阅嵌套控件结构

退出并继续

Exit For 语句会使执行退出 ForNext 循环并立即将控制转移到 Next 语句后面的语句。

Continue For 语句立即将控制转移到循环的下一次迭代。 有关详细信息,请参阅 Continue 语句

下面的示例演示如何使用 Continue ForExit For 语句。

Dim numberSeq() As Integer =
    {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}

For Each number As Integer In numberSeq
    ' If number is between 5 and 8, continue
    ' with the next iteration.
    If number >= 5 And number <= 8 Then
        Continue For
    End If

    ' Display the number.
    Debug.Write(number.ToString & " ")

    ' If number is 10, exit the loop.
    If number = 10 Then
        Exit For
    End If
Next
Debug.WriteLine("")
' Output: 1 2 3 4 9 10

可以将任意数目的 Exit For 语句放入 For Each 循环中。 在嵌套 For Each 循环中使用时,Exit For 会使执行退出最内层循环并将控制转移到下一个更高级别的嵌套。

Exit For 通常在计算某个条件后使用(例如在 If...Then...Else 结构中)。 可能需要将 Exit For 用于以下条件:

  • 继续循环访问是不必要或不可能的。 这可能是由错误值或终止请求所导致。

  • Try...Catch...Finally 中捕获了异常。可以在 Finally 块末尾使用 Exit For

  • 有一个无限循环,这是可以运行许多次甚至无限次的循环。 如果检测到此类情况,可以使用 Exit For 对循环进行转义。 有关详细信息,请参阅 Do...Loop 语句

迭代器

可使用迭代器对集合执行自定义迭代。 迭代器可以是函数或 Get 访问器。 它使用 Yield 语句返回集合的每一个元素,每次返回一个元素。

通过使用 For Each...Next 语句调用迭代器。 For Each 循环的每次迭代都会调用迭代器。 在迭代器中到达 Yield 语句时,会返回 Yield 语句中的表达式,并保留当前在代码中的位置。 下次调用迭代器时,将从该位置重新开始执行。

下面的示例使用迭代器函数。 迭代器函数具有位于 For…Next 循环中的 Yield 语句。 在 ListEvenNumbers 方法中,For Each 语句体的每次迭代都会创建一个对迭代器函数的调用,并将继续到下一个 Yield 语句。

Public Sub ListEvenNumbers()
    For Each number As Integer In EvenSequence(5, 18)
        Debug.Write(number & " ")
    Next
    Debug.WriteLine("")
    ' Output: 6 8 10 12 14 16 18
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 = firstNumber To lastNumber
        If number Mod 2 = 0 Then
            Yield number
        End If
    Next
End Function

有关详细信息,请参阅迭代器Yield 语句Iterator

技术实现

For EachNext 语句运行时,Visual Basic 会在循环开始之前只计算集合一次。 如果语句块会更改 elementgroup,则这些更改不会影响循环的迭代。

在集合中所有元素都连续分配给 element 后,For Each 循环会停止,并且控制会传递到 Next 语句后的语句。

如果 Option Infer 为 on(其默认设置),则 Visual Basic 编译器可以推断 element 的数据类型。 如果它为 off 并且 element 尚未在循环外部声明,则必须在 For Each 语句中声明它。 若要显式声明 element 的数据类型,请使用 As 子句。 除非元素的数据类型在 For Each...Next 构造外部进行定义,否则其范围是循环体。 请注意,不能同时在循环外部和内部声明 element

可以选择在 Next 语句中指定 element。 这可提高程序的可读性,尤其是在具有嵌套 For Each 循环的情况下。 指定的变量必须与出现在对应 For Each 语句中的变量相同。

你可能希望避免在循环内更改 element 的值。 这样做可能会使阅读和调试代码更加困难。 更改 group 的值不会影响集合或其元素,这些内容是在首次输入循环时确定的。

嵌套循环时,如果在内部级别的 Next 之前遇到外部嵌套级别的 Next 语句,则编译器会发出错误信号。 但是,只有在每个 Next 语句中指定 element 时,编译器才能检测此重叠错误。

如果代码依赖于按特定顺序遍历集合,则 For Each...Next 循环不是最佳选择,除非你了解集合公开的枚举器对象的特征。 遍历的顺序不是由 Visual Basic 确定,而是由枚举器对象的 MoveNext 方法确定。 因此,可能无法预测在 element 中第一个返回的集合元素,或是在给定元素之后返回的下一个元素。 可以使用不同的循环结构(如 For...NextDo...Loop)实现更可靠的结果。

运行时必须能够将 group 中的元素转换为 element。 [Option Strict] 语句控制是否允许扩大转换和收缩转换(Option Strict 为 off,这是其默认值),或者是否仅允许扩大转换(Option Strict 为 on)。 有关详细信息,请参阅收缩转换

group 的数据类型必须是引用可枚举的集合或数组的引用类型。 这通常意味着 group 引用实现 System.Collections 命名空间的 IEnumerable 接口或 System.Collections.Generic 命名空间的 IEnumerable<T> 接口的对象。 System.Collections.IEnumerable 定义 GetEnumerator 方法,该方法返回集合的枚举器对象。 枚举器对象实现 System.Collections 命名空间的 System.Collections.IEnumerator 接口,并公开 Current 属性以及 ResetMoveNext 方法。 Visual Basic 使用这些内容遍历集合。

收缩转换

Option Strict 设置为 On 时,收缩转换通常会导致编译器错误。 但是在 For Each 语句中,会在运行时计算和执行从 group 中的元素到 element 的转换,并禁止显示收缩转换导致的编译器错误。

在下面的示例中,当 Option Strict 为 on 时,将 m 分配为 n 的初始值不会进行编译,因为 LongInteger 的转换是收缩转换。 但是在 For Each 语句中,不会报告编译器错误,即使对 number 的分配需要从 LongInteger 的相同转换。 在包含较大数字的 For Each 语句中,当 ToInteger 应用于该较大数字时,会发生运行时错误。

Option Strict On

Imports System

Module Program
    Sub Main(args As String())
        ' The assignment of m to n causes a compiler error when 
        ' Option Strict is on.
        Dim m As Long = 987
        'Dim n As Integer = m

        ' The For Each loop requires the same conversion but
        ' causes no errors, even when Option Strict is on.
        For Each number As Integer In New Long() {45, 3, 987}
            Console.Write(number & " ")
        Next
        Console.WriteLine()
        ' Output: 45 3 987

        ' Here a run-time error is raised because 9876543210
        ' is too large for type Integer.
        'For Each number As Integer In New Long() {45, 3, 9876543210}
        '    Console.Write(number & " ")
        'Next
    End Sub
End Module

IEnumerator 调用

For Each...Next 循环的执行开始时,Visual Basic 会验证 group 是否引用有效的集合对象。 如果不是,则会引发异常。 否则,它会调用 MoveNext 方法和枚举器对象的 Current 属性以返回第一个元素。 如果 MoveNext 指示没有下一个元素(即集合是否为空),则 For Each 循环会停止,控制会传递到 Next 语句后的语句。 否则,Visual Basic 会将 element 设置为第一个元素,并运行语句块。

每次 Visual Basic 遇见 Next 语句时,它都会返回到 For Each 语句。 它会再次调用 MoveNextCurrent 以返回下一个元素,并根据结果再次运行语句块或停止循环。 此过程会持续,直到 MoveNext 指示没有下一个元素或遇到 Exit For 语句。

修改集合。 GetEnumerator 返回的枚举器对象通常不允许通过添加、删除、替换或重新排序任何元素来更改集合。 如果在启动了 For Each...Next 循环后更改集合,则枚举器对象会变为无效,下一次尝试访问元素时会导致 InvalidOperationException 异常。

但是,这种对修改的阻止不是由 Visual Basic 确定,而是由 IEnumerable 接口的实现确定。 可以采用允许在迭代期间进行修改的方式实现 IEnumerable。 如果考虑执行此类动态修改,请确保了解所使用的集合上的 IEnumerable 实现的特征。

修改集合元素。 枚举器对象的 Current 属性是只读的,它返回每个集合元素的本地副本。 这意味着无法在 For Each...Next 循环中修改元素自身。 进行的任何修改只会影响来自 Current 的本地副本,不会反映回基础集合中。 但是,如果元素是引用类型,则可以修改它指向的实例的成员。 下面的示例修改每个 thisControl 元素的 BackColor 成员。 但无法修改 thisControl 自身。

Sub LightBlueBackground(thisForm As System.Windows.Forms.Form)
    For Each thisControl In thisForm.Controls
        thisControl.BackColor = System.Drawing.Color.LightBlue
    Next thisControl
End Sub

上面的示例可以修改每个 thisControl 元素的 BackColor 成员,但无法修改 thisControl 自身。

遍历数组。 由于 Array 类会实现 IEnumerable 接口,因此所有数组都会公开 GetEnumerator 方法。 这意味着可以使用 For Each...Next 循环来循环访问数组。 但是只能读取数组元素。 无法更改它们。

示例 1

下面的示例使用 DirectoryInfo 类列出 C:\ 目录中的所有文件夹。

Dim dInfo As New System.IO.DirectoryInfo("c:\")
For Each dir As System.IO.DirectoryInfo In dInfo.GetDirectories()
    Debug.WriteLine(dir.Name)
Next

示例 2

以下示例阐释了对集合排序的过程。 该示例对 List<T> 中存储的 Car 类的实例进行排序。 Car 类实现 IComparable<T> 接口,此操作需要实现 CompareTo 方法。

每次对 CompareTo 方法的调用均会执行用于排序的单一比较。 CompareTo 方法中用户编写的代码针对当前对象与另一个对象的每个比较返回一个值。 如果当前对象小于另一个对象,则返回的值小于零;如果当前对象大于另一个对象,则返回的值大于零;如果当前对象等于另一个对象,则返回的值等于零。 这使你可以在代码中定义大于、小于和等于条件。

ListCars 方法中,cars.Sort() 语句对列表进行排序。 对 List<T>Sort 方法的此调用将导致为 List 中的 Car 对象自动调用 CompareTo 方法。

Public Sub ListCars()

    ' Create some new cars.
    Dim cars As New List(Of Car) From
    {
        New Car With {.Name = "car1", .Color = "blue", .Speed = 20},
        New Car With {.Name = "car2", .Color = "red", .Speed = 50},
        New Car With {.Name = "car3", .Color = "green", .Speed = 10},
        New Car With {.Name = "car4", .Color = "blue", .Speed = 50},
        New Car With {.Name = "car5", .Color = "blue", .Speed = 30},
        New Car With {.Name = "car6", .Color = "red", .Speed = 60},
        New Car With {.Name = "car7", .Color = "green", .Speed = 50}
    }

    ' Sort the cars by color alphabetically, and then by speed
    ' in descending order.
    cars.Sort()

    ' View all of the cars.
    For Each thisCar As Car In cars
        Debug.Write(thisCar.Color.PadRight(5) & " ")
        Debug.Write(thisCar.Speed.ToString & " ")
        Debug.Write(thisCar.Name)
        Debug.WriteLine("")
    Next

    ' Output:
    '  blue  50 car4
    '  blue  30 car5
    '  blue  20 car1
    '  green 50 car7
    '  green 10 car3
    '  red   60 car6
    '  red   50 car2
End Sub

Public Class Car
    Implements IComparable(Of Car)

    Public Property Name As String
    Public Property Speed As Integer
    Public Property Color As String

    Public Function CompareTo(ByVal other As Car) As Integer _
        Implements System.IComparable(Of Car).CompareTo
        ' A call to this method makes a single comparison that is
        ' used for sorting.

        ' Determine the relative order of the objects being compared.
        ' Sort by color alphabetically, and then by speed in
        ' descending order.

        ' Compare the colors.
        Dim compare As Integer
        compare = String.Compare(Me.Color, other.Color, True)

        ' If the colors are the same, compare the speeds.
        If compare = 0 Then
            compare = Me.Speed.CompareTo(other.Speed)

            ' Use descending order for speed.
            compare = -compare
        End If

        Return compare
    End Function
End Class

另请参阅