In VBA the Rows property has a weird behavior

前端 未结 3 1207
南旧
南旧 2021-01-22 04:04

I am trying to figure out how to work on a specific row among a big range. However it appears that a range created with the rows property does not behave the same as a simple ra

3条回答
  •  一整个雨季
    2021-01-22 04:16

    I cannot find any proper documentation on this, but this observed behaviour actually appears to be very logical.

    The Range class in Excel has two important properties:

    • A single instance of Range is enough to represent any possible range on a sheet
    • It is iterable (can be used in a For Each loop)

    I believe that in order to achieve logically looking iterability and yet avoid creating unnecessary entities (i.e. separate classes like CellsCollection, RowsCollection and ColumnsCollection), the Excel developers came up with a design where each instance of Range holds a private property that tells it in which units it is going to count itself (so that one range could be "a collection of rows" and another range could be "a collection of cells").

    This property is set to (say) "rows" when you create a range via the Rows property, to (say) "columns" when you create a range via the Columns property, and to (say) "cells" when you create a range in any other way.

    This allows you to do this and not become unnecessarily surprised:

    For Each r In SomeRange.Rows
      ' will iterate through rows
    Next
    
    For Each c In SomeRange.Columns
      ' will iterate through columns
    Next
    

    Both Rows and Columns here return the same type, Range, that refers to the exactly same sheet area, and yet the For Each loop iterates via rows in the first case and via columns in the second, as if Rows and Columns returned two different types (RowsCollection and ColumnsCollection).

    It makes sense that it was designed this way, because the important property of a For Each loop is that it cannot provide multiple parameters to a Range object in order to fetch the next item (cell, row, or column). In fact, For Each cannot provide any parameters at all, it can only ask "Next one please."

    To support that, the Range class had to be able to give the next "something" without parameters, even though a range is two-dimensional and needs two coordinates to fetch the "something." Which is why each instance of Range has to remember in what units it will be counting itself.

    A side effect of that design is that it is perfectly fine to look up "somethings" in a Range providing only one coordinate. This is exactly what the For Each mechanism would do, we are just directly jumping to the ith item.
    When iterating over (or indexing into) a range returned by Rows, we're going to get the ith row, from top to bottom; for a range returned by Columns we're getting the ith column, from left to right; and for a range returned by Cells or by any other method we're going to get the ith cell, counting from top left corner to the right and then to the bottom.

    Another side effect of this design is that can "step out" of a range in a meaningful way. That is, if you have a range of three cells, and you ask for the 4th cell, you still get it, and it will be the cell dictated by the shape of the range and the units it's counting itself in:

    Dim r As Range
    Set r = Range("A1:C3")          ' Contains 9 cells
    
    Debug.Print r.Cells(12).Address ' $C$4 - goes outside of the range but maintains its shape
    

    So your workaround of Set SpecificRow = Intersect(SpecificRow, SpecificRow) resets the internal counting mode of that specific Range instance from (say) "rows" to (say) "cells".

    You could have achieved the same with

    Set SpecificRow = SpecificRow.Cells
    MsgBox SpecificRow(1).Address
    

    But it's better to keep the Cells close to the point of usage rather than the point of range creation:

    MsgBox SpecificRow.Cells(1).Address
    

提交回复
热议问题