反转 For Each 循环的顺序

38

VB最强大的功能之一就是能够使用for each循环遍历集合中的对象而无需引用索引。

我发现这在仅需要删除集合中的对象时非常有用。

当从预定义的集合(例如电子表格上的行)中删除对象时,如果使用索引并从最后一个对象开始向第一个对象回溯工作,代码会更简单。(可以使用迭代器进行Step -1)(否则需要进行偏移,因为For each会在删除活动对象后将枚举器指针移回前一个对象)

例如:

For intA = 10 to 1 step -1 
    ' ...
Next

当使用 For Each | Next 时怎么办?例如:

For each rngCell in Selection.Cells
    ' ...
Next

如何使用 for each 循环语法倒序循环?


2
我甚至不知道你可以这样做:https://dev59.com/QFHTa4cB1Zd3GeqPV_Tm。好好知道一下。 - VBlades
4
@VBlades,这很有趣,但对于这个问题,如果使用新集合,问题不就一样吗?我认为在像这样删除VBA集合中的对象时,最好的解决方案是仅使用集合的索引和计数器。 - Doug Glancy
3
@DougGlancy:好的,也许是这样,Doug。说实话,我现在正在工作,只是觉得这个方法很新奇,也可能与OP的问题有关联,所以我提供了链接。不过我认为更重要的原则是,For...Each 结构是专门用于处理顺序不重要的情况(https://dev59.com/DXNA5IYBdhLWcg3wdtpd);就像带有数据行的集合理论一样 - 本质上是无序的。这意味着,如果你需要按特定顺序执行操作,请使用你正在处理的集合所提供的计数器。 - VBlades
4
为什么你想这样做——所需的解决方法将使代码比使用“Step”方法更不高效? - brettdj
谢谢大家,谁会想到语言中最强大的功能之一会如此受限。我真的很失望。 最近,我似乎提出了超出语言限制(或错误)的问题。我认为微软应该雇用我来帮助他们拓展视野。 发泄结束,感谢大家的精彩回答。 - sirplus
显示剩余4条评论
6个回答

36

使用for each循环语法无法倒序循环。

作为替代方案,可以使用For i = a To 1 Step -1循环:

Sub reverseForEach()
    Dim i As Long, rng As Range

    Set rng = ActiveSheet.Range("A1:B2")

    For i = rng.Cells.Count To 1 Step -1

        Debug.Print rng.item(i).Address
        ' Or shorthand rng(i) as the Item property 
        ' is the default property for the Range object.
        ' Prints: $B$2, $A$2, $B$1, $A$1

    Next i

End Sub

这适用于所有具有Item属性的集合,例如Worksheets、Areas或Shapes。

注意:当使用Range对象时,循环的顺序是从右向左,然后向上。


20
对于内置集合(例如 Range),简短的答案是:你无法这样做。对于用户定义的集合,@VBlades链接的答案可能会有所帮助,但成本可能超过收益。
一个解决方法是将要删除的项目的标识与实际删除分开。例如,对于范围,使用Union构建一个新的范围变量,然后处理该变量,例如一次性删除所有行。对于Range示例,您还可以利用Variant Array方法进一步加快速度。
是否有用取决于您的实际用例。

对于内置集合(例如Range),简短的回答是:你不能这样做。对于用户定义的集合,@VBlades提供的答案可能有用,尽管成本可能超过了效益。 - sirplus
1
简而言之,即使对于每个集合进行反转,也不能在删除时使用For Each,因为For Each会在活动对象被删除后将枚举器指针移回到上一个对象。这会导致偏移错误的出现。随着删除的事物越来越多,这个错误就会变得越来越大,因此需要将偏移量与计数器结合使用。在第一次使用索引和反向计数器要简单得多。 - sirplus

6

还有其他好的答案,但这里提供另一种“向后步进”Range的替代方法。


将Range反转为Array的函数

此函数返回一个"向后Range数组",可与For..Each一起使用:

Function ReverseRange(rg As Range) As Range()
    Dim arr() As Range, r As Long, c As Long, n As Long
    With rg
        ReDim arr(1 To .Cells.Count) 'resize Range Array
        For r = .Cells(.Rows.Count, 1).Row To .Cells(1, 1).Row Step -1
            For c = .Cells(1, .Columns.Count).Column To .Cells(1, 1).Column Step -1
                n = n + 1
                Set arr(n) = .Worksheet.Cells(r, c) 'set cell in Array
            Next c
        Next r
    End With
    ReverseRange = arr  'return Range Array as function result
End Function

使用示例:

Sub test()
    Dim oCell
    For Each oCell In ReverseRange(ActiveSheet.Range("E5:A1"))

        Debug.Print oCell.Address 'do something here with each cell

    Next oCell
End Sub

0

使用第二个变量作为所需的计数器,并在您的代码中使用它。

'ex: Loop from n = 19 to 16
For i = 0 To 3
   n = 19 - i
   'your code here using n as the counter
Next

0

仅适用于范围集合。如果它们有多个区域,则会更加复杂。

基本上有两个循环,第一个循环将所有单元格的索引保存在数组中,第二个循环从后往前创建范围的联合。

Option Explicit

Private Sub Main()
    Dim InvertedRange As Range
    Set InvertedRange = InvertRange(Application.Union(ActiveSheet.Range("A1:A2"), _
      ActiveSheet.Range("F6:F7"), ActiveSheet.Range("E4:F5"), ActiveSheet.Range("E1")))
    Dim ActualRange As Range
    For Each ActualRange In InvertedRange
        Debug.Print (ActualRange.Address(False, False) & " : " & ActualRange.Value)
    Next ActualRange
End Sub

Public Function InvertRange(ByVal rngRange_I As Range) As Range
    Dim RangesArray() As Long
    ReDim RangesArray(1 To rngRange_I.Count, 1 To rngRange_I.Count)
    Dim ActualArea As Range
    Dim ActualRange As Range
    Dim ArrayIndex As Long
    For Each ActualArea In rngRange_I.Areas
        For Each ActualRange In ActualArea
            ArrayIndex = ArrayIndex + 1
            RangesArray(ArrayIndex, 1) = ActualRange.Row
            RangesArray(ArrayIndex, 2) = ActualRange.Column
        Next ActualRange
    Next ActualArea

    Dim ActualRow As Long
    Dim ActualColumn As Long
    ActualRow = RangesArray(UBound(RangesArray, 1), 1)
    ActualColumn = RangesArray(UBound(RangesArray, 2), 2)
    With rngRange_I.Worksheet
        Dim InvertedRange As Range
        Set InvertedRange = .Cells(ActualRow, ActualColumn)
        For ArrayIndex = UBound(RangesArray, 1) To LBound(RangesArray, 1) Step -1
            ActualRow = RangesArray(ArrayIndex, 1)
            ActualColumn = RangesArray(ArrayIndex, 2)
            Set InvertedRange = Application.Union(InvertedRange, _
              .Cells(ActualRow, ActualColumn))
        Next ArrayIndex
    End With

    Set InvertRange = InvertedRange
End Function

0

你可以使用栈(LIFO数据结构)来创建你的列表,代码可能如下:

   Dim aStack as Object

   Set aStack = CreateObject("System.Collections.Stack") 
    
   For Each arngCell in Selection.Cells
       aStack.Push(arngCell)
   Next
    
   While aStack.Count > 0 
    
       rngCell = aStack.Pop

       ' ...
    
    End While 

    Set stack = Nothing

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接