如何在 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.Dictionary
和 Name
作为键:
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 作为一个概念是......在这里跳动(无论内部集合是 Dictionary
、Collection
还是 .NET ArrayList
通常应该是 实现细节 ,其余的的代码不需要关心)。
我怀疑 Brand
class 的职责太多,并且“是”产品集;最佳做法是将 Brand.Products
属性 定义如下:
Public Property Get Products() As Products
所以您需要 Products
class(非常类似于 Workbook.Worksheets
和 Workbook.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
数据结构。
我正在尝试将大型 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.Dictionary
和 Name
作为键:
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 作为一个概念是......在这里跳动(无论内部集合是 Dictionary
、Collection
还是 .NET ArrayList
通常应该是 实现细节 ,其余的的代码不需要关心)。
我怀疑 Brand
class 的职责太多,并且“是”产品集;最佳做法是将 Brand.Products
属性 定义如下:
Public Property Get Products() As Products
所以您需要 Products
class(非常类似于 Workbook.Worksheets
和 Workbook.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
数据结构。