如何在 VBA 中创建合适的 Collection?

How can I create a proper Collection in VBA?

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

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

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

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

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

这样的索引

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

您可以使用 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

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

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

为了支持这个:

Set shoesProduct = MyBrand.Products("Shoes")

您可以将 Products 定义为 Dictionary 并收工,但是 encapsulation 作为一个概念是......在这里跳动(无论内部集合是 DictionaryCollection 还是 .NET ArrayList 通常应该是 实现细节 ,其余的的代码不需要关心)。

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

Public Property Get Products() As Products

所以您需要 Products class(非常类似于 Workbook.WorksheetsWorkbook.Sheets 属性,两者都是 return 和 Sheets 集合对象),它封装了一个私有的模块级 VBA.Collection 字段(可能是键控的,但您不能访问或迭代集合的键)。

Products 自定义集合 class 需要一个 Item 默认 属性 (名称 Item 是公约);该实现只是从私有封装的 Collection:

中提取项目
'@DefaultMember
Public Property Get Item(ByVal Index As Variant) As Product
    Set Item = ThePrivateCollection.Item(Index)
End Property

如果您正在使用Rubberduck,这个@DefaultMember annotation/comment将触发关于注释和相应隐藏属性“不同步”的检查结果;右键单击该检查结果并选择“调整属性值”让 Rubberduck 为您生成隐藏代码并处理烦人的 export/delete-edit-reimport 循环。

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

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 class 可以使用 Lots 自定义集合 - 但由于 Lot 实际上只是一个 String 值(或者是吗?),那么 Product 可以简单地封装一个 Dictionary,并有一个 Lots 属性 公开了 Items 数组:

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

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

至于 Brands 集合本身,注意 并使用 Dictionary 数据结构。