【问题标题】:VBA for each loop with 64K+ ListRows (ouf of memory)具有 64K+ ListRows 的每个循环的 VBA(内存不足)
【发布时间】:2021-04-21 15:08:49
【问题描述】:

我正在通过 Excel 表 (Listobject) 运行 VBA for each 循环,该表根据给定路径检查文件是否存在。我的表已经扩展,并且有 68K 列表行。启动代码后很快报错Run-time-error '7': Out of memory

它运行良好,有 63K 行(在 5 分钟内完成),根据谷歌搜索,似乎有一种叫做“64K 段边界”的东西。这是什么影响了我的代码运行,因为它真的感觉它首先缓冲了行数,然后在没有开始实际运行任何东西的情况下反弹回来。是否有一个简单的解决方法,无需将我的数据集分成多个批次?坦率地说,我很惊讶 64K 限制在 2021 年仍然存在于 Excel 中。

在 64 位 Excel 2019 上运行它,但在 Office365 上也没有运气。

Sub CheckFiles()

Dim Headers As ListObject
Dim lstrw As ListRow

Dim strFileName As String
Dim strFileExists As String

Application.ScreenUpdating = False

Set ws = ThisWorkbook.Sheets("Import")
Set Headers = ws.ListObjects("Import")

    For Each lstrw In Headers.ListRows
    
        strFileName = lstrw.Range(7)
        strFileExists = Dir(strFileName)
        
        If strFileExists = "" Then
        lstrw.Range(4) = "not found"
        Else
        lstrw.Range(4) = "exists"
        End If
    
    Next lstrw

Set ws = Nothing
Set Headers = Nothing

Application.ScreenUpdating = True

End Sub

【问题讨论】:

  • 将所需数据读入数组,用循环处理数组,然后将数组连同结果一起写回单元格。使用数组时,这应该运行得快得多。每个读写操作都会带来很多开销。如果您使用数组,则开始时只有一个读取操作,最后只有一个写入操作。而不是循环中每次迭代的其中一个。
  • 如果您使用常规范围而不是列表对象,您会看到同样的情况吗?
  • 刚刚做了一个测试:可以确认行为。更改为常规的 for-loop (for i=1 to list.ListRows.Count) 没有任何问题,在 3 秒内迭代超过 70k 行。
  • @FunThomas 现在这很有趣,因为通常For Each...Next 在对象集合上比For...Next 循环快数量级。 Excel ListObject API 中是否存在内存泄漏错误?
  • For Each...Next 使用一种枚举器机制,一次生成一个对象...看起来像 ListRows 集合中的一个实现错误。

标签: excel vba foreach


【解决方案1】:

避免访问工作表

  • 由于无法避免循环,因此最好在计算机的内存中进行,即通过数组的元素而不是通过范围的单元格。
  • 代码仍然很慢,我的机器上 200k 行大约需要 10 秒,但这是因为 Dir
  • 请注意将范围写入(复制)到数组 (Data = rg.Value) 和写入(复制)有多容易(仅一行,当范围包含多个单元格时)以及多快(一瞬间)数组返回一个范围 (rg.Value = Data)。
  • 调整常量部分中的值。
Option Explicit

Sub CheckFiles()

    Const wsName As String = "Import" ' Worksheet Name
    Const tblName As String = "Import" ' Table Name
    Const cCol As Long = 7 ' Criteria Column
    Const dCol As Long = 4 ' Destination Column

    Dim wb As Workbook: Set wb = ThisWorkbook
    Dim ws As Worksheet: Set ws = wb.Worksheets(wsName)
    Dim Headers As ListObject: Set Headers = ws.ListObjects(tblName)

    Dim Data As Variant ' Data Array
    With Headers.ListColumns(cCol).DataBodyRange
        If .Rows.Count = 1 Then
            ReDim Data(1 To 1, 1 To 1): Data = .Value
        Else
            Data = .Value
        End If
    End With
    
    Dim r As Long ' Array Row Counter (DataBodyRange Row Counter)
    Dim FileName As String ' File Name Retrieved by Dir
    
    For r = 1 To UBound(Data, 1)
        FileName = Dir(CStr(Data(r, 1)))
        If Len(FileName) = 0 Then
            Data(r, 1) = "not found"
        Else
            Data(r, 1) = "exists"
        End If
    Next r
    
    Headers.ListColumns(dCol).DataBodyRange.Value = Data

End Sub

【讨论】:

    【解决方案2】:

    谢谢大家!一些外卖。虽然显然试图编写尽可能高效的代码,但这里任何合理的性能都是可以接受的。话虽如此,for each 循环运行 63K 行大约需要 5 分钟,而我接受 @VBasic2008 作为答案的代码在大约 15 秒内完成 - 也没有容量问题。

    我对这段特定代码的唯一问题是它对我来说有点新方法,因此将来可能需要在它的基础上进行深入研究——但它确实看起来很有效。我还整理了一个常规的 for ... to 循环,它也没有遇到 68K 行的问题,并且可以使用 offset 函数在行和列之间切换。

    显然比 @Pᴇʜ 建议的 for each 快​​,但花费的时间大约是数组方法的 2 倍(30 秒左右)。

    Sub CheckFiles_2()
    
    Dim strFileName, strFileExists As String
    Dim ws As Worksheet
    
    Dim Headers As ListObject
    Dim result As String
    Dim counter, RowCount As Long
    
    Application.ScreenUpdating = False
    
    Set ws = ThisWorkbook.Sheets("Import")
    Set Headers = ws.ListObjects("Import")
    
    RowCount = Headers.ListRows.Count
    
    For counter = 1 To RowCount
    
    strFileName = Range("anchorCell").Offset(counter, 3)
    
            strFileExists = Dir(strFileName)
            
            If strFileExists = "" Then
            result = "not found"
            Else
            result = "exists"
            End If
    
    Range("anchorCell").Offset(counter, 0) = result
    
    Next counter
    
    Set ws = Nothing
    Set Headers = Nothing
    
    Application.ScreenUpdating = True
    
    End Sub
    

    【讨论】:

    • 除非现在删除了评论,否则 Pᴇʜ 的评论解释了为什么使用数组更快:范围被读取一次并写入一次,因此工作表仅被访问两次(当然还有更多数据) while 循环遍历范围会读取和写入每一行的数据一次(例如,访问工作表给或取 130,000 次)。当使用对象并使用For...Next 循环时,可能总是可以使用应该更快的For Each...Next 循环来编写代码。明天我会在我的答案中添加这样的解决方案,以便您自己比较。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-01-25
    • 2012-02-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-19
    相关资源
    最近更新 更多