VBA 动态数组错误地复制了一些值

VBA dynamic array duplicating some values in error

首先我想说我完全不知道为什么我的代码正在做它正在做的事情。我真的希望这里的一位 VBA 专家能提供帮助。另外,这是我的第一个post,所以我尽力遵守规则,但如果我做错了什么,请指出。


我有一个子程序,它遍历一列数据并创建一个数组。它调用一个函数来检查特定值是否已经在数组中。如果不是,则重新确定数组的尺寸,插入值,然后重新开始该过程,一直持续到到达列表末尾。我最终得到一个总计 41 个值的数组,但其中 4 个已被复制两次,因此数组中只有 37 个唯一值。

我一辈子都弄不明白是什么让这些价值观与众不同,或者为什么它们会被重复。总列表有 700 多个值,所以我想我应该看到其他值重复,但我没有。

下面是创建数组的子代码:

Sub ProductNumberArray(strWrkShtName As String, strFindColumn As String, blAsGrp As Boolean, iStart As Integer)
    Dim i As Integer
    Dim lastRow As Integer
    Dim iFindColumn As Integer
    Dim checkString As String

    With wbCurrent.Worksheets(strWrkShtName)
        iFindColumn = .UsedRange.Find(strFindColumn, .Range("A1"), xlValues, xlWhole, xlByColumns).Column
        lastRow = .Cells(Rows.Count, iFindColumn).End(xlUp).row
        For i = iStart To lastRow
            checkString = .Cells(i, iFindColumn).Value
            If IsInArray(checkString, arrProductNumber) = False Then
                If blAsGrp = False Then
                    ReDim Preserve arrProductNumber(0 To j)
                    arrProductNumber(j) = checkString
                    j = j + 1
                Else
                    ReDim Preserve arrProductNumber(1, 0 To j)
                    arrProductNumber(0, j) = .Cells(i, iFindColumn - 1).Value
                    arrProductNumber(1, j) = checkString
                    j = j + 1
                End If
            End If
        Next i
    End With
End Sub

下面是检查 checkString 值是否在数组中的代码:

Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
    Dim bDimen As Byte, i As Long

    On Error Resume Next
    If IsError(UBound(arr, 2)) Then bDimen = 1 Else bDimen = 2
    On Error GoTo 0

    Select Case bDimen
    Case 1
        On Error Resume Next
        IsInArray = Application.Match(stringToBeFound, arr, 0)
        On Error GoTo 0
    Case 2
        For i = 1 To UBound(arr, 2)
            On Error Resume Next
            IsInArray = Application.Match(stringToBeFound, Application.Index(arr, , i), 0)
            On Error GoTo 0
            If IsInArray = True Then Exit For
        Next
    End Select
End Function

我们非常欢迎任何帮助。我以前能够找到我所有问题的答案(或者至少调试并看到一个明显的问题)但是这个问题难倒了我。我希望有人能弄清楚这是怎么回事。


[编辑] 这是调用 sub 的代码:

Sub UpdatePSI()    
    Set wbCurrent = Application.ActiveWorkbook
    Set wsCurrent = wbCurrent.ActiveSheet

    frmWorkbookSelect.Show

    If blFrmClose = True Then 'if the user closes the selection form, the sub is exited
        blFrmClose = False
        Exit Sub
    End If

    Set wsSelect = wbSelect.Sheets(1)

    Call ProductNumberArray("Forecast", "Item", True, 3)

wbCurrentwsCurrentblFrmClose 在一般声明中定义。

问题

您正在检查变量数组中的字符串。数据可以是字符串或数字,因此会重复。我建议将函数 Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean 更改为 Function IsInArray(stringToBeFound As Variant, arr() As Variant) As Boolean

有几个变量需要声明。见下文。

Sub ProductNumberArray(strWrkShtName As String, strFindColumn As String, blAsGrp As Boolean, iStart As Integer)
Dim i As long, j as long 'just use long for i.  integers are silently converted to long anyway.  leaving j undeclared makes it variant.
Dim lastRow As Integer
Dim iFindColumn As Integer
Dim checkString As Variant ' changed to variant
Dim arrProductNumber() as Variant ' delcare a dynamic array

ReDim arrProductNumber(0 To 0) ' making it an array

j = 0 'giving somewhere to start

With wbCurrent.Worksheets(strWrkShtName)
    iFindColumn = .UsedRange.Find(strFindColumn, .Range("A1"), xlValues, xlWhole, xlByColumns).Column
    lastRow = .Cells(Rows.Count, iFindColumn).End(xlUp).row
    For i = iStart To lastRow
        checkString = .Cells(i, iFindColumn).Value
        If IsInArray(checkString, arrProductNumber) = False Then
            If blAsGrp = False Then
                ReDim Preserve arrProductNumber(0 To j)
                arrProductNumber(j) = checkString
                j = j + 1
            Else
                ReDim Preserve arrProductNumber(1, 0 To j)
                arrProductNumber(0, j) = .Cells(i, iFindColumn - 1).Value
                arrProductNumber(1, j) = checkString
                j = j + 1
            End If
        End If
    Next i
End With
End Sub

我猜你得到了重复项,因为 jarrProductNumber 是全局变量。您应该通过将工作表传递给将 return 您的数组的函数来摆脱全局变量。

您只需将单元格引用添加到 Scripting.Dictionary

If not dic.Exists(Cell.Value) then dic.Add Cell.Value, Cell

然后通过它的键值检索引用

ProductOffset = dic("PID798YD").Offset(0,-1)

我在这里使用 ArrayList(我本可以使用 Scripting.Dictionary)来检查重复项并作为 Redim 多维数组的计数器。


Sub TestgetProductData()
    Dim results As Variant
    results = getProductData(ActiveSheet, "Column 5", True, 3)
    Stop
    results = getProductData(ActiveSheet, "Column 5", False, 3)
    Stop
End Sub

Function getProductData(ws As Worksheet, ColumnHeader As String, blAsGrp As Boolean, iStart As Integer) As Variant
    Dim results As Variant
    Dim cell As Range, Source As Range
    Dim list As Object
    Set list = CreateObject("System.Collections.ArrayList")

    With ws.UsedRange
        Set Source = .Find(ColumnHeader, .Range("A1"), xlValues, xlWhole, xlByColumns)
        If Not Source Is Nothing Then
            Set Source = Intersect(.Cells, Source.EntireColumn)
            Set Source = Intersect(.Cells, Source.Offset(iStart))
            For Each cell In Source
                If Not list.Contains(cell.Value) Then

                    If blAsGrp Then
                        If list.Count = 0 Then ReDim results(0 To 1, 0 To 0)

                        ReDim Preserve results(0 To 1, 0 To list.Count)
                        results(0, list.Count) = cell.Offset.Value
                        results(1, list.Count) = cell.Value
                    End If
                    list.Add cell.Value
                End If
            Next
        End If
    End With
    If blAsGrp Then
        getProductData = results
    Else
        getProductData = list.ToArray
    End If
End Function

根据@RonRosenfield 和@braX 的建议,我尝试了 Scripting.Dictionary 并得出了这个答案。它既创建又检查值,这与我以前使用子创建和检查函数的方法不同。

Sub ProductNumberDictionary(strWrkShtName As String, strFindCol As String, blAsGrp As Boolean, iStart As Integer)
    Dim i As Integer
    Dim iLastRow As Integer
    Dim iFindCol As Integer
    Dim strCheck As String

    Set dictProductNumber = CreateObject("Scripting.Dictionary")

    With wbCurrent.Worksheets(strWrkShtName)
        iFindCol = .UsedRange.Find(strFindCol, .Cells(1, 1), xlValues, xlWhole, xlByColumns).Column
        iLastRow = .Cells(Rows.Count, iFindCol).End(xlUp).row
        For i = iStart To iLastRow
            strCheck = .Cells(i, iFindCol).Value
            If dictProductNumber.exists(strCheck) = False Then
                If blAsGrp = False Then
                    dictProductNumber.Add Key:=strCheck
                Else
                    dictProductNumber.Add Key:=strCheck, Item:=.Cells(i, iFindCol - 1).Value
                End If
            End If
        Next
    End With
End Sub

我在从这本字典中获取值时遇到了一些困难,但发现这行得通:

    Dim o as Variant
    i = 0
    For Each o In dictProductNumber.Keys
        .Cells(iRowStart + i, iFirstCol + 1) = o 'returns the value of the key
        .Cells(iRowStart + i, iFirstCol + 2) = dictProductNumber(o) 'returns the item stored with the key
        i = i + 1
    Next

None 关于导致您遇到的重复问题的(疯狂)猜测甚至接近。它实际上是由您的代码中的错误引起的。

在您的 IsInArray 函数中,您以错误的值完成了数组循环索引。 For i = 1 To UBound(arr, 2) 应该是 For i = 1 To UBound(arr, 2) - LBound(arr, 2) + 1。当您的索引完成一个短时,这意味着永远不会针对最后一个数组项检查比较字符串,因此,任何连续相同值中的第二个将作为副本复制。始终在索引参数中同时使用 LBoundUBound 以避免此类错误和类似类型的错误。


但是,此修复是多余的,因为可以重写函数以避免完全循环。我还添加了一些其他增强功能:

Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
  Dim bDimen As Long
  Dim i As Long

  On Error Resume Next
    bDimen = 2
    If IsError(UBound(arr, 2)) Then bDimen = bDimen - 1
    If IsError(UBound(arr, 1)) Then bDimen = bDimen - 1
  On Error GoTo 0

  Select Case bDimen
    Case 0:
    ' Uninitialized array - return false
    Case 1:
      On Error Resume Next
        IsInArray = Application.Match(stringToBeFound, arr, 0)
      On Error GoTo 0
    Case 2:
      On Error Resume Next
        IsInArray = Application.Match(stringToBeFound, Application.Index(arr, 2), 0)
      On Error GoTo 0
    Case Else
      ' Err.Raise vbObjectError + 666, Description:="Never gets here error."
  End Select
End Function

这是我对字典解决方案的看法:

Public Function ProductNumberDict _
                ( _
                           ByVal TheWorksheet As Worksheet, _
                           ByVal Header As String, _
                           ByVal AsGroup As Boolean, _
                           ByVal Start As Long _
                ) _
        As Scripting.Dictionary

  Set ProductNumberDict = New Scripting.Dictionary
  With TheWorksheet.Rows(1).Cells(WorksheetFunction.Match(Header, TheWorksheet.Rows(1), 0)).EntireColumn
    Dim rngData As Range
    Set rngData = TheWorksheet.Range(.Cells(Start), .Cells(Rows.Count).End(xlUp))
  End With
  Dim rngCell As Range
  For Each rngCell In rngData
    With rngCell
      If Not ProductNumberDict.Exists(.Value2) Then
        ProductNumberDict.Add .Value2, IIf(AsGroup, .Offset(, -1).Value2, vbNullString)
      End If
    End With
  Next rngCell
End Function

调用函数的方法如下:

Sub UpdatePSI()

  Dim wkstForecast As Worksheet
  Set wkstForecast = ActiveWorkbook.Worksheets("Forecast")

' ...

  Dim dictProductNumbers As Scripting.Dictionary
  Set dictProductNumbers = ProductNumberDict(wkstForecast, "Item", False, 7)
  Set dictProductNumbers = ProductNumberDict(wkstForecast, "Item", True, 3)

  Dim iRowStart As Long: iRowStart = 2
  Dim iFirstCol As Long: iFirstCol = 5
  With wkstForecast.Cells(iRowStart, iFirstCol).Resize(RowSize:=dictProductNumbers.Count)
  .Offset(ColumnOffset:=1).Value = WorksheetFunction.Transpose(dictProductNumbers.Keys)
  .Offset(ColumnOffset:=2).Value = WorksheetFunction.Transpose(dictProductNumbers.Items)
  End With

' ...

End Sub

特别注意用于将字典内容复制到工作表的非循环方法。