【问题标题】:Excel VBA Programming with Arrays: To Pass them or Not To Pass them?Excel VBA 数组编程:通过还是不通过?
【发布时间】:2010-01-22 22:48:19
【问题描述】:

问题:我想知道在 Excel 2003 VBA 中处理数组的最佳解决方案是什么

背景:我在 Excel 2003 中有一个超过 5000 行的宏。我在过去 2 年中构建了它,将新功能添加为新过程,这有助于分割代码并调试、更改或添加到该功能。缺点是我在多个过程中使用了许多相同的基本信息,这需要我多次将其加载到具有微小差异的数组中。我现在遇到了运行时间长度的问题,所以我现在可以进行完全重写。
该文件用于抓取多项制造流程(最多 4 个不同的设置,总共最多 10 个不同的流程,每个流程最多 1000 个步骤),信息是特定于流程的,特定于分组/排序的子流程用途和数据(例如移动、库存、CT、...)
然后,它将数据粘贴到用于管理流程的多张工作表上,利用要阅读的数据表、图表和单元格格式来表示流程能力/历史记录。
流程在 Excel 文件中,而制造数据通过 7 种不同的 OO4O Oracle SQL 拉取读取,其中一些重复使用多次

数组是:
arrrFlow(1 to 1000, 1 to 4) 作为具有 4 个字符串的记录类型
arrrSubFlow(1 到 1000, 1 到 10) 作为记录类型,包含 4 个字符串、2 个整数和 1 个单
arrrData(1 到 1000, 1 到 10) 作为记录类型,具有 1 个字符串、4 个整数、12 个长整数和 1 个单字节
arriSort(1 to 1000, 1 to 4) as Integer(用作指针数组,以组、子组和步骤顺序对流、子流和数据进行排序,同时保持原始数组按步骤顺序)

可能性:
1) 将宏重写为一个大过程,该过程将数据加载到在过程中标注的主数组中一次
Pro:在过程中标注而不是作为模块中的公共变量并且未通过。
缺点:使用一个大型程序而不是多个较小的程序更难调试。

2) 使用多个过程保留宏,但传递数组
优点:使用多个较小的程序更容易调试代码。
缺点:传递数组(昂贵?)

3) 使用多个过程保留宏,但数组是模块中的公共变暗变量
优点:使用多个较小的程序更容易调试代码。
缺点:公共数组(昂贵?)

那么,社区的结论是什么?有谁知道使用公共数组与传递数组的费用?是否值得失去让我的程序专注于一个功能的便利性?

更新:
我在离散级别加载库存数据(每个步骤多个),在聚合级别移动数据(每个步骤一个),在聚合级别加载班次开始库存。我通过逐步将库存数据置于工作状态类别(运行、等待、...)中来聚合库存数据,并根据工作表上已有的数据创建目标。

我有一个按类型显示工作流程的流程图,目前 3 个产品具有相似但不完全相同的流程,2 个产品是不同的流程,它们相似但又不相同。我已经为不同流程中的每组步骤分配了一个组和子组。

我将这些数据放在多张纸上,有些按步骤顺序,有些按组/子组顺序。我还需要按组和产品、组/子组和产品、部分生产线和产品以及产品汇总的数据。

我使用记录类型,所以我实际上有一个可读的三维数组,arrSubFlow(1,1).strStep(第一个设备的第一个步骤的步骤名称),arrData(10,5).lngYest(昨天的移动第 5 个装置的第 10 步)。

我的主要优化点将在我每次从头开始创建 10 个页面的部分。合并单元格、边框、标题……这是一个非常耗时的过程。我将添加一个部分,将我的数据与页面进行比较,以查看是否需要更改,如果需要,则仅重新创建它,否则,我将清除每个数据部分并仅将更改的数据写入工作表。根据我的时间记录数据,这将是巨大的。但是,每当我更新代码时,我总是尝试改进代码的其他方面。我认为将数据加载到结构 (Array, RecordSet, Collection) once 既是一点点优化,但对于数据完整性而言更是如此,所以我没有机会加载它不同的工作表不同。

我现在看到远离数组的主要问题是:
* 已经对它们进行了大量投资,但这不是不改变的充分理由
* 不知道通过它们是否有很多成本,因为它将由 ByRef
* 我使用一个排序函数来创建一个排序的“指针”数组,它让我可以按照步骤流顺序离开数组,同时可以轻松地按组/子组顺序引用它。

由于我一直在尝试为现在和未来编写代码,我并不反对将数组更新为 RecordSet 或 Collections,而不仅仅是为了改变它们来学习一些很酷的东西。我的阵列有效,根据我的研究,它们增加了运行时间,而不是这个 2 分钟报告的大量时间。因此,如果将来另一个结构比记录类型的二维数组更容易更新,那么请告诉我,但是有没有人知道将数组传递给过程的成本,假设你不做 ByVal 传递?

【问题讨论】:

  • 克雷格,请您花 5 分钟时间尝试一下我概述的破解代码统计性能方法并将结果发布在这里吗?此外,如果有任何方法可以隐藏您的代码以在此处共享,那将是很棒的。或者,如果你能获得许可与一个人分享,我很高兴看到我能做些什么来提供帮助。你想让我签署保密协议吗?
  • 令人失望的是,您从未尝试过我所说的。当人们如此确信自己的正确性以至于他们甚至不会尝试某事时,这真是令人沮丧。
  • 对不起,我从这个急需的重写中被拉到其他活动上。他们甚至不会给我时间去做我理解的事情,更不用说学习更好的方法了。现在他们对 Excel 发誓。我非常感谢您为教我更好的方法所付出的努力,我希望其他人也能从中受益。
  • 直到今天我才看到您的最终评论。很抱歉,您永远无法在这方面取得更多进展。

标签: arrays excel vba


【解决方案1】:

您已经提供了很多细节,但如果不看一些代码,仍然很难准确理解发生了什么。在您的问题中,我可以确定您贯穿始终的至少 4 个大主题:制造、数据访问、VBA 和编码最佳实践。我很难确切地说出你在问什么,因为你的问题范围很大。无论哪种方式,我都感谢您尝试在 VBA 中编写更好的代码。

我很难准确理解您打算如何处理这些数组。你说:

缺点是我在多个过程中使用了许多相同的基本信息,这需要我多次将其加载到具有微小差异的数组中。

我不确定你在这里的意思。您是否使用数组来表示从数据库中检索到的一行数据?如果是这样,您可能会考虑使用 类模块 而不是通常的“宏”模块。这些将允许您使用成熟的对象而不是值数组(或引用,视情况而定)。类需要更多的工作来设置和使用,但它们使您的代码更易于使用,并且将极大地帮助您对代码进行分段。

正如用户 Emtucifor 已经指出的那样,可能有诸如 ADO Recordset 对象之类的对象(可能需要安装 Access...不确定)可以提供很大帮助。或者您可以创建自己的。

这是一个很长的示例,说明如何使用类可能会对您有所帮助。虽然这个例子很长,但它将向您展示一些面向对象编程的原则如何真正帮助您清理代码。

在 VBA 编辑器中,转到 Insert > Class Module。在“属性”窗口(默认情况下位于屏幕左下方)中,将模块名称更改为WorkLogItem。将以下代码添加到类中:

Option Explicit

Private pTaskID As Long
Private pPersonName As String
Private pHoursWorked As Double

Public Property Get TaskID() As Long
    TaskID = pTaskID
End Property

Public Property Let TaskID(lTaskID As Long)
    pTaskID = lTaskID
End Property

Public Property Get PersonName() As String
    PersonName = pPersonName
End Property

Public Property Let PersonName(lPersonName As String)
    pPersonName = lPersonName
End Property

Public Property Get HoursWorked() As Double
    HoursWorked = pHoursWorked
End Property

Public Property Let HoursWorked(lHoursWorked As Double)
    pHoursWorked = lHoursWorked
End Property

上面的代码将为我们提供一个特定于我们正在处理的数据的强类型对象。当您使用多维数组存储数据时,您的代码类似于:arr(1,1) 是 ID,arr(1,2) 是 PersonName,arr(1,3) 是 HoursWorked。使用这种语法,很难知道什么是什么。假设您仍然将对象加载到数组中,而是使用我们在上面创建的WorkLogItem。这个名字,你可以通过arr(1).PersonName 得到这个人的名字。这使您的代码更易于阅读。

让我们继续这个例子。我们将尝试使用collection,而不是将对象存储在数组中。

接下来,添加一个新的类模块并将其命名为ProcessWorkLog。将以下代码放入其中:

Option Explicit

Private pWorkLogItems As Collection

Public Property Get WorkLogItems() As Collection
    Set WorkLogItems = pWorkLogItems
End Property

Public Property Set WorkLogItems(lWorkLogItem As Collection)
    Set pWorkLogItems = lWorkLogItem
End Property

Function GetHoursWorked(strPersonName As String) As Double
    On Error GoTo Handle_Errors
    Dim wli As WorkLogItem
    Dim doubleTotal As Double
    doubleTotal = 0
    For Each wli In WorkLogItems
        If strPersonName = wli.PersonName Then
            doubleTotal = doubleTotal + wli.HoursWorked
        End If
    Next wli

Exit_Here:
    GetHoursWorked = doubleTotal
        Exit Function

Handle_Errors:
        'You will probably want to catch the error that will '
        'occur if WorkLogItems has not been set '
        Resume Exit_Here


End Function

上面的类将用于“做某事”,集合为WorkLogItem。最初,我们只是将其设置为计算工作总小时数。让我们测试我们编写的代码。创建一个新模块(这次不是类模块;只是一个“常规”模块)。将以下代码粘贴到模块中:

Option Explicit

Function PopulateArray() As Collection
    Dim clnWlis As Collection
    Dim wli As WorkLogItem
    'Put some data in the collection'
    Set clnWlis = New Collection

    Set wli = New WorkLogItem
    wli.TaskID = 1
    wli.PersonName = "Fred"
    wli.HoursWorked = 4.5
    clnWlis.Add wli

    Set wli = New WorkLogItem
    wli.TaskID = 2
    wli.PersonName = "Sally"
    wli.HoursWorked = 3
    clnWlis.Add wli

    Set wli = New WorkLogItem
    wli.TaskID = 3
    wli.PersonName = "Fred"
    wli.HoursWorked = 2.5
    clnWlis.Add wli

    Set PopulateArray = clnWlis
End Function

Sub TestGetHoursWorked()
    Dim pwl As ProcessWorkLog
    Dim arrWli() As WorkLogItem
    Set pwl = New ProcessWorkLog
    Set pwl.WorkLogItems = PopulateArray()
    Debug.Print pwl.GetHoursWorked("Fred")

End Sub

在上面的代码中,PopulateArray() 只是创建了一个WorkLogItem 的集合。在您的真实代码中,您可能会创建类来解析 Excel 工作表或数据对象以填充集合或数组。

TestGetHoursWorked() 代码只是演示了如何使用这些类。您注意到ProcessWorkLog 被实例化为一个对象。实例化后,WorkLogItem 的集合成为pwl 对象的一部分。您在Set pwl.WorkLogItems = PopulateArray() 行中注意到了这一点。接下来,我们简单地调用我们编写的函数,该函数作用于集合WorkLogItems

为什么会有帮助?

假设您的数据发生了变化,并且您想添加一个新方法。假设您的 WorkLogItem 现在包含 HoursOnBreak 的字段,并且您想添加一个新方法来计算它。

您需要做的就是向WorkLogItem 添加一个属性,如下所示:

Private pHoursOnBreak As Double

Public Property Get HoursOnBreak() As Double
    HoursOnBreak = pHoursOnBreak
End Property

Public Property Let HoursOnBreak(lHoursOnBreak As Double)
    pHoursOnBreak = lHoursOnBreak
End Property

当然,您需要更改填充集合的方法(我使用的示例方法是PopulateArray(),但您可能应该为此设置一个单独的类)。然后你只需将你的新方法添加到你的 ProcessWorkLog 类中:

Function GetHoursOnBreak(strPersonName As String) As Double
     'Code to get hours on break
End Function

现在,如果我们想更新我们的 TestGetHoursWorked() 方法以返回 GetHoursOnBreak 的结果,我们只需添加以下行:

    Debug.Print pwl.GetHoursOnBreak("Fred")

如果您传入代表数据的值数组,则必须在代码中找到使用数组的每个位置,然后相应地更新它。如果您改用类(及其实例化对象),则可以更轻松地更新代码以处理更改。此外,当您允许以多种方式使用该类时(可能一个函数只需要 4 个对象属性,而另一个函数需要 6 个),它们仍然可以引用同一个对象。这样可以避免为不同类型的函数创建多个数组。

为了进一步阅读,我强烈建议获取VBA Developer's Handbook, 2nd edition 的副本。这本书充满了很好的例子和最佳实践以及大量的示例代码。如果您为一个严肃的项目投入大量时间在 VBA 上,那么值得您花时间阅读这本书。

【讨论】:

  • 感谢您对本书的建议,假设它与 Excel 2003 相关,我将对其进行研究。请参阅我对您的一些观点和问题的问题的更新。
  • @Craig - 这本书仍然与 Excel 2003 相关。从 Excel 2000 到 Excel 2003 的 VBA 可能会有细微的变化,但总体上是一样的。 --- 当我给出答案时,我已经阅读了你更新的帖子。同样,如果没有任何代码,仍然很难看到您在说什么。 ---我的回答是为了展示使用类和传递对象如何比传递多维数组更具冒险性。它确实对我过去的编码有所帮助,但有一个学习曲线。
  • 抱歉,我的意思是我目前正在处理的更新我刚刚完成,我添加了一个更新部分来尝试充实它。对于没有代码,我很抱歉,有超过 5000 行,所以我不知道该显示什么,而且我的公司非常重视安全......我会看看这本书。
  • ADO 将在除了最奇怪的 Windows 安装之外的所有设备上,因此不需要 MS Access。本的回答正是我所做的。我无法忍受试图记住数组的哪些元素是什么。考虑这段代码 dblCost = arrItems(1,57) * arrItems(3,57) 或 dblCost = clsItem.Quantity & clsItem.UnitCost 我知道我宁愿调试哪个(并为此从头开始编写)。您可以有效地在过程之间传输类,因为您只传输指针 - byref。
  • @Craig 几件事:1)也许我只是不了解制造过程以了解您在做什么(即使在阅读了您的新更新之后-两次),但我根本无法提供更多的是没有看到它。很明显,这是一个复杂的过程。 2)你提到编辑标题和其他页面布局的东西。这总是很耗时,并且与数组、集合等无关。 3) 如果这是我的项目(而且我有很多类似的项目),我会考虑使用 MS Access,特别是如果你正在使用数据聚合。可能在 .NET 中查看 VB 或 C# 以获得更好的工具。
【解决方案2】:

听起来 Excel 和数组可能不是您正在做的工作的最佳工具。如果您能解释一下您正在使用的数据类型以及您正在做什么,那将真正有助于提供更好的答案。尽可能详细地说明您对数据进行的操作类型以及输入和输出是什么。

我将提供一些我认为对您有帮助的重点,然后可能会在收到您的回复后编辑我的答案以使其更加完整,这样我就有更多时间充实一些内容。

  • 有一个对象可以自然地处理您正在使用的记录类型对象,称为 Recordset。在 VBA 编辑器中,转到工具 -> 参考并添加 Microsoft ActiveX 数据对象 2.X 库(您机器上最高的库)。您可以声明一个 ADODB.Recordset 类型的对象,然后执行 Recordset.Fields.Append 向其中添加字段,然后 .Open 它,最后 .AddNew、设置字段值和 .Update。这是在程序中作为输入或输出参数传递的自然对象。它具有自然遍历和定位功能(.Eof、.Bof、.AbsolutePosition、.MoveNext、.MoveFirst、.MovePrevious)并支持搜索和过滤(.Filter = "Field = 'abc'"、.Find 等)。

  • 我不建议使用公共变量,但如果不了解你在做什么,我在这里不能给你很好的建议。

  • 我也会避免一个大程序。代码应该被分解成只做一件事的可重用功能单元,它们的名字本质上是关于它们所做的事情的自我记录。

  • 如果您想提高代码的性能,请在代码运行时随机按 ctrl-break 并中断代码。然后按 Ctrl-L 查看调用堆栈。每次记下列表中的内容。如果任何项目大部分时间都出现,那么它就是瓶颈,您应该花时间尝试优化它。但是,我不建议您在做出一些更高级别的决定(例如是否切换到记录集)之前尝试优化您拥有的东西。

我真的需要更多信息来更好地帮助你。

如果您有兴趣,我会编写一些演示代码来展示 Recordset 对象的实用性。使用 Recordset.GetRows 或 .GetString 将 Recordset 中的数据插入 Excel 范围非常容易(尽管可能需要一些数组转置,但这也不难)。

更新:如果您的目标是加快您的流程,那么在做任何事情之前,我认为最好掌握最耗时的知识。请按 ctrl-break 大约 10 次并记下每次调用堆栈,然后告诉我调用堆栈中最常见的项目是什么?

在更新单元格格式的速度方面,这是我的经验:

  1. 合并是您可以做的最慢的操作。如果可能的话,尽量避免它。使用“选择中心”是一种选择。另一个只是不合并,而是使用适当的大小、边框、单元格背景颜色和关闭整个工作簿的网格线的组合。

  2. 将边框或其他格式应用于可能的最大事物,而不是像逐个单元格这样的许多小事物。例如,如果大多数单元格都有所有边框,但有些没有,则将所有边框应用于整个范围,并在循环期间删除不需要的边框。即便如此,尝试做整行和更大的范围。

  3. 保存已应用边框和格式的模板文件。假设您在其中放置了一行,其中包含特定部分的格式。在一个步骤中将该行复制到该部分所需的尽可能多的行中,例如 20 行,并且它们都将具有相同的格式。复制行比逐个单元格地应用格式要快得多。

另外,我不会自动选择使用类。虽然 OO 很棒而且我自己做(哎呀,前几天我刚刚为某个东西构建了 8 个类来建模一个层次结构,这样我就可以在需要时轻松地公开它的各个部分),但实际上它可能会更慢。类中的一组简单公共变量比使用 getter 和 setter 更快。用户定义的 Type 甚至比类更快,但您可能会遇到试图在类中传递 UDT 的陷阱(它们必须在非类公共模块中声明,即使那样它们也会产生问题)。

【讨论】:

  • 我已根据您的问题更新了我的帖子,提供了更多详细信息。感谢 Ctrl-L,但我已经创建了一个计时日志文件,它告诉我哪些功能是最长的。我知道从 OO4O 将这些数据加载到工作表上,然后从工作表中多次加载,并进行一些积累和修改,既效率低下,又可能导致输出不同(最坏的情况)。所以我想把它加载到一张纸上,然后加载到另一个结构中,目前是数组。根据我的经验和您的评论,我应该保留多个程序,并通过结构或将它们公开。
  • @Craig:我过去所有的计时工作都是以同样的方式完成的。但我想如果你尝试我概述的方法,你会对结果感到非常惊讶。请阅读stackoverflow.com/questions/266373/…stackoverflow.com/questions/375913/…stackoverflow.com/questions/926266/…stackoverflow.com/questions/890222/…
  • 在他的情况下类可能会更好,因为他可以使用 Recordset.GetRows 属性来加载数组,其中数组是对象的属性。然后,他可以从代码中的任何位置访问该数组,方法是通过对象访问它。
  • @Lance Roberts:使用 Recordset 对象与创建自己的类不会有什么不同吗?我不确定你的建议是什么。
  • @Emtudifor,我建议在加载 Recordset 的对象中有一个方法,并调用 GetRows 方法将其加载到作为同一对象属性的数组中。然后为了访问该数组,他访问对象的属性。抱歉我之前不够清楚。
猜你喜欢
  • 2018-03-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-20
  • 1970-01-01
  • 2011-07-20
  • 2012-06-05
  • 2016-08-04
  • 1970-01-01
相关资源
最近更新 更多