我可以在 Excel VBA 中使用什么函数来对数组进行切片?
Application.WorksheetFunction.Index(数组,行,列)
如果为行或列指定零值,那么您将获得指定的整个列或行。
示例:
Application.WorksheetFunction.Index(数组, 0, 3)
这将为您提供整个第三列。
如果您将行和列都指定为非零,那么您将仅获得特定元素。 没有简单的方法可以得到比完整的行或列更小的切片。
限制:如果您使用的是较新版本的 Excel,
WorksheetFunction.Index
可以处理的数组大小存在限制。如果 array
的行数超过 65,536 行或列数超过 65,536 列,则会抛出“类型不匹配”错误。如果这对您来说是个问题,请参阅这个更复杂的答案,它不受相同的限制。
这是我编写的用于执行所有 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
下面是一种对 Excel 变体数组进行切片的快速方法。其中大部分内容是使用这个优秀网站的信息组合而成的http://bytecomb.com/vba-reference/
本质上,目标数组是预先构建为空的一维或二维变量,并与源数组和要切片的元素索引一起传递给子数组。由于数组在内存中的存储方式,对列进行切片比对行进行切片要快得多,因为内存布局允许复制单个块。
这样做的好处是它可以远远超出 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
有两件事,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
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
这是另一种方法。
这不是多维的,但适用于单行和单列。
f 和 t 参数是从零开始的。
Function slice(ByVal arr, ByVal f, ByVal t)
slice = Application.Index(arr, Evaluate("Transpose(Row(" & f + 1 & ":" & t + 1 & "))"))
End Function
这是我编写的一个漂亮的函数,用于对二维数组进行子集化
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
这是一个老问题,但如果您想将范围的 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 结合使用,您可以快速逐行读取整个范围。
与许多其他最新语言不同,数组没有直接的
slice
函数。
但是,有一个简短的代码片段对此非常方便。 下面是一维数组的完整解决方案:
'*************************************************************
'* 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
您可以使用行、列、偏移和调整大小属性的组合来获取范围的子集。
例如,如果您的范围为 5 列 x 3 行:
Set rng = Range("A1:E3")
通过适当组合上述属性,可以得到任意子集。例如,如果您想获取第二行最右边的 3 个单元格(即上例中的“C2:E2”),您可以执行以下操作:
Set rngSubset = rng.Rows(2).Offset(0, rng.Columns.Count - 3).Resize(1, 3)
然后您可以将其包装在 VBA 函数中。
我只会创建一个与您需要的切片一样长的数组。然后循环遍历它,从完整数组中复制值。完整数组的索引将是切片应开始的位置(在我的示例中为 1)。 所以,如果你的完整数组是(“a”,“b”,“c”,“d”)并且你需要“b”和“c”:
Dim slice(1) as Variant
For i = 0 To 1
slice(i) = fullArray( i + 1)
Next
这不是一般情况,而是我在 Excel 中的常见用例。我更喜欢一个更具声明性的解决方案。但接受的答案并没有为我产生一个应用于内存数组的数组。如果这是重复的,我深表歉意,但我在这里寻找但没有找到它。
Public Function Get1Darray_From2ndColumnOf_2Darray(ByVal argAx2D As Variant) As Variant
Dim Ax1D_() As Variant
ReDim Preserve Ax1D_(0 To UBound(argAx2D, 2))
Dim ix
For ix = LBound(argAx2D, 2) To UBound(argAx2D, 2)
Ax1D_(ix) = argAx2D(0, ix)
Next ix
Get1Darray_From2ndColumnOf_2Darray = Ax1D_
End Function