数组已初始化:
Private pSectors(1 To 3) As clsNewSector
它的单个元素没有被分配,但数组本身已经准备好你想扔给它的任何东西:它在编译时为clsNewSector 实例保留了 3 个位置。作为一个对象类型,数组仅仅持有指针。在底层,VBA 数组指向一个类似于这样的SAFEARRAY 结构:
typedef struct tagSAFEARRAY {
USHORT cDims;
USHORT fFeatures;
ULONG cbElements;
ULONG cLocks;
PVOID pvData;
SAFEARRAYBOUND rgsabound[1];
} SAFEARRAY;
任何给定元素的类型元数据只能通过一系列连续的指针读取来访问;数组结构本身抽象出元素的 type 概念,因此它能够支持 any 类型。 This content(不是我的)详细说明。
编程语言是否可以让Foo 进入Bar 对象数组完全取决于类型安全,其中 .在 .NET(和其他技术)中,类型安全性得到了非常广泛的探索,包括泛型和其他高级机制。在 25 年前的 VBA 中,我们的编译器将大量类型安全性推迟到最后一分钟的运行时检查——这段代码编译得非常好:
Public Sub Test()
Dim things(1 To 10) As Class1
Set things(1) = New Class2 '<~ *run-time* error 13 "type mismatch"
End Sub
现在,如果存在某种程度的运行时类型安全,那肯定意味着类型元数据存在于某处!确实如此 - 但 VBA 并没有广泛的类型系统出现在该语言中,而且它的大部分内部机制都不是[容易] 以编程方式访问的(想到调用堆栈)。
在任何函数调用中,首先计算参数 - 所以当我们执行 TypeName(a(1)) 时,a(1) 下标解析为 NULL 对象指针(还没有引用 Set),所以 Nothing 被传递给TypeName 函数,它返回字符串 "Nothing",因为 VBA 在这方面有很奇怪的打字语义——但是如果我们暂时不考虑单个元素的类型并查看数组本身的数据类型......
Public Sub Test()
Dim things(1 To 10) As Class1
Debug.Print TypeName(things) ' prints "Object()"
End Sub
我们显式类型数组的报告数据类型是Object()这一事实告诉我们我们需要知道的一切:数组本身的真实类型元数据正在被抽象掉,所以有没有 [simple?] VBA 代码可以给定一个数组和一个对象变量,并确定对象是否是数组的正确类型,除非我们硬编码预期的数据类型 并且不在乎对于空引用。
我们在这里面临的问题是,我们需要确定特定数据类型的原因是生成该类型的新对象 - 由于对象引用初始化为 Nothing,这是唯一的“数据类型”我们只会得到。
除非我们以不同的方式初始化数组。
您可以让clsNewSector 有一个默认实例(将其VB_PredeclaredId 属性值设置为True,或者只使用和同步Rubberduck 的@PredeclaredId annotation,然后公开这样的属性:
Public Property Get IsDefault() As Boolean
IsDefault = Me Is clsNewSector
End Property
然后你可以尽快用指向类的默认实例的指针初始化pSectors:
For i = LBound(pSectors) To UBound(pSectors)
Set pSectors(i) = clsNewSector
Next
这样,当您将a(1) 传递给TypeName 时,没有下标默认为Nothing,然后剩下的代码就会被诅咒,必须验证他们正在查看的实例是否具有IsDefault = True,否则我们冒着使该全局默认实例 有状态 的风险,并且我们不希望这种情况发生(在各种数组索引处共享全局状态 - 可能会出现什么问题?!)。
整体比较虚弱,绝对不理想。
一个惯用的解决方案是create a custom collection class per type,它可以根据自己的条件强制执行类型安全。可能是NewSectors 类(考虑删除cls 前缀),其Item 索引Property Set 成员将在给定Nothing 或任何不是NewSector 实例的对象引用时抛出错误。
问题的根源在于NewObject 函数有太多的明显职责,它似乎可以创建任何给定具有类型名称的字符串。
让我们跳出框框思考一下。
Function InitializeObjectArray(a As Variant) As Variant
我们使用数组a 本身来获取一个类型的名称,然后我们将该名称传递给一个函数,该函数可以获取我们期望的类型的对象。
所以这个函数本质上是与NewObject 耦合的,它的名字并不能告诉我们它的适用范围。
这看起来像是……的工作
如果我们把这个职责转移到一个正式的接口上会怎样?
@Interface IArrayItemFactory
Option Explicit
Public Function Create() As Object
End Function
那么我们可以有一个NewSectorFactory 类来实现它,如下所示:
Implements IArrayItemFactory
Option Explicit
Private Function IArrayItemFactory_Create() As Object
Set IArrayItemFactory_Create = New clsNewSector
End Function
现在InitializeObjectArray 可能看起来像这样:
Public Function InitializeObjectArray(ByVal a As Variant, ByVal factory As IArrayItemFactory) As Variant
For i = LBound(a) To UBound(a)
Set a(i) = factory.Create
Next i
End Function
通过剥离函数计算要创建的对象类型的职责,并将其移动到自己的对象/依赖项中,该函数现在可以轻松地初始化任何具有实现的对象类型的数组IArrayItemFactory 接口,调用代码现在负责告诉函数用什么来初始化数组,只需负责为工厂提供实现即可。