【问题标题】:How do I slice an array in Excel VBA?如何在 Excel VBA 中对数组进行切片?
【发布时间】:2010-09-15 13:36:15
【问题描述】:

我可以在 Excel VBA 中使用什么函数来对数组进行切片?

【问题讨论】:

    标签: arrays excel vba worksheet-function


    【解决方案1】:

    Application.WorksheetFunction.Index(数组、行、列)

    如果您为行或列指定零值,那么您将获得指定的整个列或行。

    例子:

    Application.WorksheetFunction.Index(array, 0, 3)

    这将为您提供整个第 3 列。

    如果您将行和列都指定为非零,那么您将仅获得特定元素。 没有比整行或整列更小的切片的简单方法。

    限制:如果您使用的是较新版本的 Excel,WorksheetFunction.Index 可以处理的数组大小存在限制。如果array 的行数超过 65,536 行或列数超过 65,536,则会引发“类型不匹配”错误。如果这对您来说是个问题,请参阅不受相同限制的this more complicated answer

    这是我编写的用于执行所有 1D 和 2D 切片的函数:

    Public Function GetArraySlice2D(Sarray As Variant, Stype As String, Sindex As Integer, Sstart As Integer, Sfinish As Integer) As Variant
    
    ' this function returns a slice of an array, Stype is either row or column
    ' Sstart is beginning of slice, Sfinish is end of slice (Sfinish = 0 means entire
    ' row or column is taken), Sindex is the row or column to be sliced
    ' (NOTE: 1 is always the first row or first column)
    ' an Sindex value of 0 means that the array is one dimensional 3/20/09 ljr
    
    Dim vtemp() As Variant
    Dim i As Integer
    
    On Err GoTo ErrHandler
    
    Select Case Sindex
        Case 0
            If Sfinish - Sstart = UBound(Sarray) - LBound(Sarray) Then
                vtemp = Sarray
            Else
                ReDim vtemp(1 To Sfinish - Sstart + 1)
                For i = 1 To Sfinish - Sstart + 1
                    vtemp(i) = Sarray(i + Sstart - 1)
                Next i
            End If
        Case Else
            Select Case Stype
                Case "row"
                    If Sfinish = 0 Or (Sstart = LBound(Sarray, 2) And Sfinish = UBound(Sarray, 2)) Then
                        vtemp = Application.WorksheetFunction.Index(Sarray, Sindex, 0)
                    Else
                        ReDim vtemp(1 To Sfinish - Sstart + 1)
                        For i = 1 To Sfinish - Sstart + 1
                            vtemp(i) = Sarray(Sindex, i + Sstart - 1)
                        Next i
                    End If
                Case "column"
                    If Sfinish = 0 Or (Sstart = LBound(Sarray, 1) And Sfinish = UBound(Sarray, 1)) Then
                        vtemp = Application.WorksheetFunction.Index(Sarray, 0, Sindex)
                    Else
                        ReDim vtemp(1 To Sfinish - Sstart + 1)
                        For i = 1 To Sfinish - Sstart + 1
                            vtemp(i) = Sarray(i + Sstart - 1, Sindex)
                        Next i
                    End If
            End Select
    End Select
    GetArraySlice2D = vtemp
    Exit Function
    
    ErrHandler:
        Dim M As Integer
        M = MsgBox("Bad Array Input", vbOKOnly, "GetArraySlice2D")
    
    End Function
    

    【讨论】:

    • 那么为什么不直接使用 redim preserve?
    • @Jon49:因为ReDim Preserve 仅适用于数组的最后一维。此外,您只能通过更改上限来更改数组的大小;所以你不能随便挑任何一列。
    • @VBOG,它在某些情况下是循环的,但在它可以使用索引功能的情况下它会更快,所以总体上会更快。您实际上可以使用内存调用重写整个事情以真正提高速度,但我不需要那种速度来做我正在做的事情。
    • @LanceRoberts。我的问题是 Index 函数本身是否更快,而您的答案是肯定的!不确定内存调用是什么,但我可以确保我的所有数组都是“索引友好”构建的。
    【解决方案2】:

    您可以结合使用 Rows、Columns、Offset 和 Resize 属性来获取范围的子集。

    例如,如果您的范围是 5 列乘 3 行:

    Set rng = Range("A1:E3")
    

    您可以通过适当组合上述属性来获得任何子集。例如,如果您想获取第二行最右边的 3 个单元格(即上例中的“C2:E2”),您可以执行以下操作:

       Set rngSubset = rng.Rows(2).Offset(0, rng.Columns.Count - 3).Resize(1, 3)
    

    然后您可以将其封装在 VBA 函数中。

    【讨论】:

      【解决方案3】:

      有两件事,VBA 不支持数组切片,所以无论您使用什么,都必须自己动手。但由于这仅适用于 Excel,因此您可以使用内置的工作表函数索引进行数组切片。

      Sub Test()
          'All example return a 1 based 2D array.
          Dim myArr As Variant 'This var must be generic to work.
          'Get whole range:
          myArr = ActiveSheet.UsedRange
          'Get just column 1:
          myArr = WorksheetFunction.Index(ActiveSheet.UsedRange, 0, 1)
          'Get just row 5
          myArr = WorksheetFunction.Index(ActiveSheet.UsedRange, 5, 0)
      End Sub
      

      【讨论】:

        【解决方案4】:

        Lance 的解决方案有一个错误,它不尊重具有未指定长度的子数组的偏移起始值,我还发现它的工作原理相当混乱。我在下面提供了一个(希望)更透明的解决方案。

        Public Function GetSubTable(vIn As Variant, Optional ByVal iStartRow As Integer, Optional ByVal iStartCol As Integer, Optional ByVal iHeight As Integer, Optional ByVal iWidth As Integer) As Variant
            Dim vReturn As Variant
            Dim iInRowLower As Integer
            Dim iInRowUpper As Integer
            Dim iInColLower As Integer
            Dim iInColUpper As Integer
            Dim iEndRow As Integer
            Dim iEndCol As Integer
            Dim iRow As Integer
            Dim iCol As Integer
        
            iInRowLower = LBound(vIn, 1)
            iInRowUpper = UBound(vIn, 1)
            iInColLower = LBound(vIn, 2)
            iInColUpper = UBound(vIn, 2)
        
            If iStartRow = 0 Then
                iStartRow = iInRowLower
            End If
            If iStartCol = 0 Then
                iStartCol = iInColLower
            End If
        
            If iHeight = 0 Then
                iHeight = iInRowUpper - iStartRow + 1
            End If
            If iWidth = 0 Then
                iWidth = iInColUpper - iStartCol + 1
            End If
        
            iEndRow = iStartRow + iHeight - 1
            iEndCol = iStartCol + iWidth - 1
        
            ReDim vReturn(1 To iEndRow - iStartRow + 1, 1 To iEndCol - iStartCol + 1)
        
            For iRow = iStartRow To iEndRow
                For iCol = iStartCol To iEndCol
                    vReturn(iRow - iStartRow + 1, iCol - iStartCol + 1) = vIn(iRow, iCol)
                Next
            Next
        
            GetSubTable = vReturn
        End Function
        

        【讨论】:

        • +1 用于蛮力方法。但是我不明白你对兰斯回答中所谓的错误的解释。
        【解决方案5】:

        以下是对 Excel 变体数组进行切片的快速方法。其中大部分是使用来自这个优秀网站http://bytecomb.com/vba-reference/的信息放在一起的@

        本质上,目标数组被预先构建为一个空的 1d 或 2d 变体,并与源数组和要切片的元素索引一起传递给 sub。由于数组存储在内存中的方式,对列进行切片比对行进行切片要快得多,因为内存布局允许复制单个块。

        这样做的好处是它的扩展性远远超出了 Excel 行数限制。

        Option Explicit
        
        #If Win64 Then
            Public Const PTR_LENGTH As Long = 8
            Public Declare PtrSafe Function GetTickCount Lib "kernel32" () As Long
            Public Declare PtrSafe Sub Mem_Copy Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Any, ByRef Source As Any, ByVal Length As Long)
            Private Declare PtrSafe Function VarPtrArray Lib "VBE7" Alias "VarPtr" (ByRef Var() As Any) As LongPtr
            Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
            Private Declare PtrSafe Sub FillMemory Lib "kernel32" Alias "RtlFillMemory" (Destination As Any, ByVal Length As Long, ByVal Fill As Byte)
        #Else
            Public Const PTR_LENGTH As Long = 4
            Public Declare Function GetTickCount Lib "kernel32" () As Long
            Public Declare Sub Mem_Copy Lib "kernel32" Alias "RtlMoveMemory" (ByRef Destination As Any, ByRef Source As Any, ByVal Length As Long)
            Private Declare Function VarPtrArray Lib "VBE7" Alias "VarPtr" (ByRef Var() As Any) As LongPtr
            Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
            Private Declare Sub FillMemory Lib "kernel32" Alias "RtlFillMemory" (Destination As Any, ByVal Length As Long, ByVal Fill As Byte)
        #End If
        
        Private Type SAFEARRAYBOUND
            cElements    As Long
            lLbound      As Long
        End Type
        
        Private Type SAFEARRAY_VECTOR
            cDims        As Integer
            fFeatures    As Integer
            cbElements   As Long
            cLocks       As Long
            pvData       As LongPtr
            rgsabound(0) As SAFEARRAYBOUND
        End Type
        
        Sub SliceColumn(ByVal idx As Long, ByRef arrayToSlice() As Variant, ByRef slicedArray As Variant)
        'slicedArray can be passed as a 1d or 2d array
        'sliceArray can also be part bound, eg  slicedArray(1 to 100) or slicedArray(10 to 100)
        Dim ptrToArrayVar As LongPtr
        Dim ptrToSafeArray As LongPtr
        Dim ptrToArrayData As LongPtr
        Dim ptrToArrayData2 As LongPtr
        Dim uSAFEARRAY As SAFEARRAY_VECTOR
        Dim ptrCursor As LongPtr
        Dim cbElements As Long
        Dim atsBound1 As Long
        Dim elSize As Long
        
            'determine bound1 of source array (ie row Count)
            atsBound1 = UBound(arrayToSlice, 1)
            'get pointer to source array Safearray
            ptrToArrayVar = VarPtrArray(arrayToSlice)
            CopyMemory ptrToSafeArray, ByVal ptrToArrayVar, PTR_LENGTH
            CopyMemory uSAFEARRAY, ByVal ptrToSafeArray, LenB(uSAFEARRAY)
            ptrToArrayData = uSAFEARRAY.pvData
            'determine byte size of source elements
            cbElements = uSAFEARRAY.cbElements
        
            'get pointer to destination array Safearray
            ptrToArrayVar = VarPtr(slicedArray) + 8 'Variant reserves first 8bytes
            CopyMemory ptrToSafeArray, ByVal ptrToArrayVar, PTR_LENGTH
            CopyMemory uSAFEARRAY, ByVal ptrToSafeArray, LenB(uSAFEARRAY)
            ptrToArrayData2 = uSAFEARRAY.pvData
        
            'determine elements size
            elSize = UBound(slicedArray, 1) - LBound(slicedArray, 1) + 1
            'determine start position of data in source array
            ptrCursor = ptrToArrayData + (((idx - 1) * atsBound1 + LBound(slicedArray, 1) - 1) * cbElements)
            'Copy source array to destination array
            CopyMemory ByVal ptrToArrayData2, ByVal ptrCursor, cbElements * elSize
        
        End Sub
        
        Sub SliceRow(ByVal idx As Long, ByRef arrayToSlice() As Variant, ByRef slicedArray As Variant)
        'slicedArray can be passed as a 1d or 2d array
        'sliceArray can also be part bound, eg  slicedArray(1 to 100) or slicedArray(10 to 100)
        Dim ptrToArrayVar As LongPtr
        Dim ptrToSafeArray As LongPtr
        Dim ptrToArrayData As LongPtr
        Dim ptrToArrayData2 As LongPtr
        Dim uSAFEARRAY As SAFEARRAY_VECTOR
        Dim ptrCursor As LongPtr
        Dim cbElements As Long
        Dim atsBound1 As Long
        Dim i As Long
        
            'determine bound1 of source array (ie row Count)
            atsBound1 = UBound(arrayToSlice, 1)
            'get pointer to source array Safearray
            ptrToArrayVar = VarPtrArray(arrayToSlice)
            CopyMemory ptrToSafeArray, ByVal ptrToArrayVar, PTR_LENGTH
            CopyMemory uSAFEARRAY, ByVal ptrToSafeArray, LenB(uSAFEARRAY)
            ptrToArrayData = uSAFEARRAY.pvData
            'determine byte size of source elements
            cbElements = uSAFEARRAY.cbElements
        
            'get pointer to destination array Safearray
            ptrToArrayVar = VarPtr(slicedArray) + 8 'Variant reserves first 8bytes
            CopyMemory ptrToSafeArray, ByVal ptrToArrayVar, PTR_LENGTH
            CopyMemory uSAFEARRAY, ByVal ptrToSafeArray, LenB(uSAFEARRAY)
            ptrToArrayData2 = uSAFEARRAY.pvData
        
            ptrCursor = ptrToArrayData + ((idx - 1) * cbElements)
            For i = LBound(slicedArray, 1) To UBound(slicedArray, 1)
        
                CopyMemory ByVal ptrToArrayData2, ByVal ptrCursor, cbElements
                ptrCursor = ptrCursor + (cbElements * atsBound1)
                ptrToArrayData2 = ptrToArrayData2 + cbElements
            Next i
        
        End Sub
        

        示例用法:

        Sub exampleUsage()
        Dim sourceArr() As Variant
        Dim destArr As Variant
        Dim sliceIndex As Long
        
            On Error GoTo Err:
        
            sourceArr = Sheet1.Range("A1:D10000").Value2
            sliceIndex = 2 'Slice column 2 / slice row 2
        
            'Build target array
            ReDim destArr(20 To 10000) '1D array from row 20 to 10000
        '    ReDim destArr(1 To 10000) '1D array from row 1 to 10000
        '    ReDim destArr(20 To 10000, 1 To 1) '2D array from row 20 to 10000
        '    ReDim destArr(1 To 10000, 1 To 1) '2D array from row 1 to 10000
        
            'Slice Column
            SliceColumn sliceIndex, sourceArr, destArr
        
            'Slice Row
            ReDim destArr(1 To 4)
            SliceRow sliceIndex, sourceArr, destArr
        
        Err:
            'Tidy Up See ' http://stackoverflow.com/questions/16323776/copy-an-array-reference-in-vba/16343887#16343887
            FillMemory destArr, 16, 0
        
        End Sub
        

        使用以下测试在旧的双核 CPU 上计时

        Sub timeMethods()
        Const trials As Long = 10
        Const rowsToCopy As Long = 1048576
        Dim rng As Range
        Dim Arr() As Variant
        Dim newArr As Variant
        Dim newArr2 As Variant
        Dim t As Long, t1 As Long, t2 As Long, t3 As Long
        Dim i As Long
        
            On Error GoTo Err
        
            'Setup Conditions 1time only
            Sheet1.Cells.Clear
            Sheet1.Range("A1:D1").Value = Split("A1,B1,C1,D1", ",") 'Strings
        '    Sheet1.Range("A1:D1").Value = Split("1,1,1,1", ",") 'Longs
            Sheet1.Range("A1:D1").AutoFill Destination:=Sheet1.Range("A1:D" & rowsToCopy), Type:=xlFillDefault
        
            'Build source data
            Arr = Sheet1.Range("A1:D" & rowsToCopy).Value
            Set rng = Sheet1.Range("A1:D" & rowsToCopy)
        
            'Build target container
            ReDim newArr(1 To rowsToCopy)
            Debug.Print "Trials=" & trials & " Rows=" & rowsToCopy
            'Range
            t3 = 0
            For t = 1 To trials
                t1 = GetTickCount
        
                    For i = LBound(newArr, 1) To UBound(newArr, 1)
                        newArr(i) = rng(i, 2).Value2
                    Next i
        
                t2 = GetTickCount
                t3 = t3 + (t2 - t1)
                Debug.Print "Range: " & t2 - t1
            Next t
            Debug.Print "Range Avg ms: " & t3 / trials
        
            'Array
            t3 = 0
            For t = 1 To trials
                t1 = GetTickCount
        
                    For i = LBound(newArr, 1) To UBound(newArr, 1)
                        newArr(i) = Arr(i, 2)
                    Next i
        
                t2 = GetTickCount
                t3 = t3 + (t2 - t1)
                Debug.Print "Array: " & t2 - t1
            Next t
            Debug.Print "Array Avg ms: " & t3 / trials
        
            'Index
            t3 = 0
            For t = 1 To trials
                t1 = GetTickCount
        
                    newArr2 = WorksheetFunction.Index(rng, 0, 2) 'newArr2 2d
        
                t2 = GetTickCount
                t3 = t3 + (t2 - t1)
                Debug.Print "Index: " & t2 - t1
            Next t
            Debug.Print "Index Avg ms: " & t3 / trials
        
            'CopyMemBlock
            t3 = 0
            For t = 1 To trials
                t1 = GetTickCount
        
                    SliceColumn 2, Arr, newArr
        
                t2 = GetTickCount
                t3 = t3 + (t2 - t1)
                Debug.Print "CopyMem: " & t2 - t1
            Next t
            Debug.Print "CopyMem Avg ms: " & t3 / trials
        
        Err:
            'Tidy Up
            FillMemory newArr, 16, 0
        
        
        End Sub
        

        【讨论】:

        • 对于@user3357963 的任何人,感谢您提供这个非常独特的答案
        【解决方案6】:

        这是另一种方式。

        这不是多维的,但可以单行单列工作。

        f 和 t 参数是从零开始的。

        Function slice(ByVal arr, ByVal f, ByVal t)
            slice = Application.Index(arr, Evaluate("Transpose(Row(" & f + 1 & ":" & t + 1 & "))"))
        End Function
        

        【讨论】:

          【解决方案7】:

          这是我编写的一个用于子集二维数组的漂亮函数

          Function Subset2D(arr As Variant, Optional rowStart As Long = 1, Optional rowStop As Long = -1, Optional colIndices As Variant) As Variant
              'Subset a 2d array (arr)
              'If rowStop = -1, all rows are returned
              'colIndices can be provided as a variant array like Array(1,3)
              'if colIndices is not provided, all columns are returned
          
              Dim newarr() As Variant, newRows As Long, newCols As Long, i As Long, k As Long, refCol As Long
          
              'Set the correct rowStop
              If rowStop = -1 Then rowStop = UBound(arr, 1)
          
              'Set the colIndices if they were not provided
              If IsMissing(colIndices) Then
                  ReDim colIndices(1 To UBound(arr, 2))
                  For k = 1 To UBound(arr, 2)
                      colIndices(k) = k
                  Next k
              End If
          
              'Get the dimensions of newarr
              newRows = rowStop - rowStart + 1
              newCols = UBound(colIndices) + 1
              ReDim newarr(1 To newRows, 1 To newCols)
          
              'Loop through each empty element of newarr and set its value
              For k = 1 To UBound(newarr, 2) 'Loop through each column
                  refCol = colIndices(k - 1) 'Get the corresponding reference column
                  For i = 1 To UBound(newarr, 1) 'Loop through each row
                      newarr(i, k) = arr(i + rowStart - 1, refCol) 'Set the value
                  Next i
              Next k
          
              Subset2D = newarr
          End Function
          

          【讨论】:

            【解决方案8】:

            与许多其他最近的语言不同,数组没有直接的slice 函数。

            但是,有一个短代码 sn-p 非常方便。 下面是一维数组的完整解决方案:

            '*************************************************************
            '*                      Fill(N1,N2)
            '* Create 1 dimension array with values from N1 to N2 step 1
            '*************************************************************
            Function Fill(N1 As Long, N2 As Long) As Variant
             Dim Arr As Variant
             If N2 < N1 Then
               Fill = False
               Exit Function
             End If
             Fill = WorksheetFunction.Transpose(Evaluate("Row(" & N1 & ":" & N2 & ")"))
            End Function
            
            '**********************************************************************
            '*                        Slice(AArray, [N1,N2])
            '* Slice an array between indices N1 to N2
            '***********************************************************************
            Function Slice(VArray As Variant, Optional N1 As Long = 1, Optional N2 As Long = 0) As Variant
             Dim Indices As Variant
             If N2 = 0 Then N2 = UBound(VArray)
             If N1 = LBound(VArray) And N2 = UBound(VArray) Then
               Slice = VArray
             Else
               Indices = Fill(N1, N2)
               Slice = WorksheetFunction.Index(VArray, 1, Indices)
             End If
            End Function
            

            用于测试

            Var V As Variant
            V = Fill(100,109)
            PrintArr(Slice(V,3,5))
            
            '************************************************
            '*                 PrintArr(VArr)
            '* Print the array VARR
            '**************************************************
            Function PrintArr(VArray As Variant)
             Dim S As String
             S = Join(VArray, ", ")
             MsgBox (S)
            End Function
            

            结果

            102, 103, 104 
            

            【讨论】:

              【解决方案9】:

              这是一个老问题,但如果您想将范围的 1 行检索到一维数组中,您可以使用 Index 和 Transpose 来实现。

              Sub test()
                  Dim ar1
                  Dim a As Object: Set a = Application
              
                  ar1 = a.Transpose(a.Transpose(a.Index(Range("A1:C3"), 2, 0)))  'get 2d row
                  Debug.Print Join(ar1, "|")
              End Sub
              

              将其与 OFFSET 结合使用,您可以逐行快速读取整个范围。

              【讨论】:

              猜你喜欢
              • 2010-11-23
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2013-09-20
              相关资源
              最近更新 更多