【问题标题】:How can I create a proper Collection in VBA?如何在 VBA 中创建正确的集合?
【发布时间】:2021-06-01 18:00:22
【问题描述】:

我正在尝试将大型 3 维数组转换为一系列类模块。我将每个下一个类存储为上一个类中的一个数组。它就像品牌 -> 产品 -> 很多。

我已经成功创建了这个交互,并且可以通过如下名称访问它们:

Sub test()
    Dim MyBrand As Brand
    Set MyBrand = New Brand
    MyBrand.Name = "Company1"
    MyBrand.AddProduct "Shoes"
    MyBrand.Products("Shoes").AddLot "240502"
    MsgBox MyBrand.Products("Shoes").Lots(0) 'Correctly Displays "240502"
End Sub

但后来我想创建一个对象组,可以保存多个Brand 对象并像Brands("Company1") 一样访问它们。

如果我在类模块中使用数组,我最终会得到Brands.Brand("Company1")。 如果我使用Collection,我将不得不使用像Brands(1) 这样的索引。

有没有办法创建适当的对象组,以便我可以模仿 Application.Workbooks 等组的语法并按名称引用成员?

【问题讨论】:

  • 您可以使用Scripting.DictionaryName 作为键。
  • 为什么不用Collection.Add item, key,然后key获取元素呢?
  • 如果您有可能出现重复键,我建议您使用内置的字典 .exists()
  • 是的,只是尝试使用键的集合,看起来字典更胜一筹。当我尝试检查 MyCollection("New Name") is Nothing 时出现错误。这意味着我无法像使用 MyDict.Exists("New Name") 那样测试它。
  • 这里有很好的概述 - codevba.com/help/collection.htm

标签: excel vba collections


【解决方案1】:

自定义集合背后的许多魔力取决于您无法在 VBE 中编辑的隐藏属性;您需要导出(并在出现提示时从项目中删除)类模块,在 Notepad/Notepad++ 中编辑其魔术成员属性,保存更改,然后将模块重新导入项目中。

这显然很乏味且容易出错,但有一个(很多)更好的方法。

为了支持这个:

Set shoesProduct = MyBrand.Products("Shoes")

您可以将Products 定义为Dictionary 并收工,但是封装 作为一个概念... DictionaryCollection 或 .NET ArrayList 通常应该是其余代码不需要关心的实现细节

我怀疑Brand 类的职责太多,并且“是”产品集合;最佳做法是将Brand.Products 属性定义如下:

Public Property Get Products() As Products

所以你会想要一个 Products 类(非常像 Workbook.WorksheetsWorkbook.Sheets 属性都返回一个 Sheets 集合对象),它封装了一个私有的模块级 VBA.Collection 字段(可能是键控,但您不能访问或迭代集合的键)。

Products 自定义集合类需要一个Item 默认属性(名称Item 是一个约定);实现只是从私有封装的Collection 中提取项目:

'@DefaultMember
Public Property Get Item(ByVal Index As Variant) As Product
    Set Item = ThePrivateCollection.Item(Index)
End Property

如果您使用Rubberduck,则此@DefaultMember 注释/注释将触发关于注释和相应隐藏属性“不同步”的检查结果;右键单击该检查结果并选择“调整属性值”,让 Rubberduck 为您生成隐藏代码并处理烦人的导出/删除-编辑-重新导入循环。

否则,您需要手动编辑隐藏的VB_UserMemId 成员属性,使其成为类的默认成员

Public Property Get Item(ByVal Index As Variant) As Product
    Attribute Item.VB_UserMemId = 0
    Set Item = ThePrivateCollection.Item(Index)
End Property

这样,MyBrand.Products("Shoes") 就等同于 MyBrand.Products.Item("Shoes")

也许您也想迭代集合中的所有产品?

For Each Product In MyBrand.Products
    Debug.Print Product.Name
Next

为了做到这一点,你需要一个特殊的“枚举器”成员来转发封装集合中的枚举器:

'@Enumerator
Public Property Get NewEnum() As IUnknown
    Set NewEnum = ThePrivateCollection.[_NewEnum]
End Property

同样,Rubberduck 注释大大简化了此操作,但 Rubberduck 所做的一切,如果您愿意,您也可以手动完成:

Public Property Get NewEnum() As IUnknown
    Attribute NewEnum.VB_UserMemId = -4
    Set NewEnum = ThePrivateCollection.[_NewEnum]
End Sub

现在For Each 迭代适用于您的自定义对象集合!

如果 Lot 不仅仅是一个 String 值(即实际的对象类型),那么 Product 类也可以使用 Lots 自定义集合 - 但因为 Lot 真的只是一个String 值(或者是?),然后Product 可以简单地封装一个Dictionary,并有一个Lots 属性公开Items 数组:

Public Property Get Lots() As Variant
    Lots = ThePrivateLotsDictionary.Items
End Property

注意,这比使用Collection 更简单,因为对于集合,您需要对其进行迭代并将每个项目复制到一个数组中,以便在不暴露集合本身的情况下返回项目(暴露 Lots() As Collection 会使AddLot 成员完全多余)。

至于Brands 集合本身,请注意Tim Williams' advice 并使用Dictionary 数据结构。

【讨论】:

  • 当我第一次看到你的答案时,我不得不设置一个提醒,以便在获得更多课程模块经验后回来。经过半个月的学习,我觉得自己对其中蕴含的智慧还只是半途而废。感谢您的出色回答,我在学习更多之后必须再次回来。
  • 欢迎来到俱乐部。马特的回答几乎总是这样。我必须至少阅读和研究它们 3 次,而且我在 SO 中跟随他已经快 5 年了。我建议你去拿一份橡皮鸭,学习 VBA 和 OOP 的生活会轻松 10 倍。他的博客也是有关该主题的重要知识来源
【解决方案2】:

您可以使用Scripting.DictionaryName 作为键:

Sub test()
    
    Dim MyBrand As Brand
    Dim Brands As Object
    
    Set Brands = CreateObject("scripting.dictionary")
    
    Set MyBrand = New Brand
    MyBrand.Name = "Company1"
    MyBrand.AddProduct "Shoes"
    MyBrand.Products("Shoes").AddLot "240502"
    
    Brands.Add MyBrand.Name, MyBrand
    
    MsgBox Brands("Company1").Products("Shoes").Lots(0)

End Sub

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-02-19
    • 1970-01-01
    相关资源
    最近更新 更多