【问题标题】:Determine the type of an uninitialized array in VBA确定VBA中未初始化数组的类型
【发布时间】:2020-07-28 03:29:22
【问题描述】:

我想编写一个函数,当为用户定义的类提供一个已声明但未初始化的数组时,该函数将识别该数组旨在保存的对象类型,然后为该数组创建新对象。 我有一个名为“clsNewSector”的类,我在另一个类中声明了一个数组,如下所示:

Private pSectors(1 To 3) As clsNewSector

然后我有一个函数应该能够识别虽然每个对象此时都引用“Nothing”,但它应该能够获取数组的类型,在这种情况下是 clsNewSector,但在其他情况下类类型可能会改变。

Function InitializeObjectArray(a As Variant) As Variant
Dim NameofType As String
NameOfType = TypeName(a(1))
For i = LBound(a) To UBound(a)
    Set a(i) = NewObject(NameOfType)
Next i
End Function

typename 函数不起作用,因为它只返回“Nothing”,因为数组未初始化,它无法识别变量被声明为不同的类型。有什么方法可以找到未初始化数组的声明类型,以便我可以使用 for 循环使用正确类型的新对象初始化数组的每个成员。

【问题讨论】:

  • @MathieuGuindon ,但这仍然不能解决基于传递给函数的数组创建不同对象的问题。
  • 我认为 Mathieu 的意思是您的处理方式错误,您的问题是XY problem
  • @BigBen 正确,这不是一个假定甚至需要解决的问题,而是由制造的问题false 需要有一个函数负责根据不存在的类型元数据创建不同类型的对象:IMO 需要另一种方法。
  • 如果您提供数组变量而不是数组的成员,Typename 将(以某种方式)起作用。不幸的是,在对象(任何类型)的情况下,从未初始化的数组返回的类型名将是 'Object()。
  • 我能想到的NewObject 的任何实现都意味着Select Case 块涉及越来越多的可能异构和不相关的数据类型,因此将函数/模块耦合到尽可能多的类,所以气味确实是由该方法引起的高级耦合 - 总体思路是一个很好的(增加抽象级别 / 减少高级代码的冗长),但是在 VBA 中使用类型系统的元数据很快就会遇到语言限制......

标签: arrays excel vba typename


【解决方案1】:

OP 提出的问题本质上是关于初始化一个对象,该对象(在这种情况下)是 clsNewSector 的集合。

一种简单的方法是将 scripting.dictionary 包装在强类型类中。

下面的代码用于一个名为 clsNewSectors 的对象,它将创建一个对象,其中包含一个包含三个 clsNewSector 对象的脚本字典。

脚本字典的 Item 属性是强类型的,因此我们只能添加、设置或获取 clsNewSector 对象。

如果您检查下面的代码,您会发现它可以通过仅在 ClsNewSector 上进行搜索和替换来重新用于不同的对象(假设仍然可以接受整数长键)

类 clsNewSectors

Option Explicit

Private Type State
    
    Host As Scripting.dictionary
    
End Type

Private s As State



Private Sub Class_Initialize()

    Set s.Host = New Scripting.dictionary
    
    With s.Host
    
        .Add .Count, New clsNewSector
        .Add .Count, New clsNewSector
        .Add .Count, New clsNewSector
        
    End With
    
End Sub

Public Property Get Item(ByVal ipIndex As Long) As clsNewSector

    If s.Host.exists(ipIndex) Then
    
        Set Item = s.Host.Item(ipIndex)
        
    Else
    
        Err.Raise 381, "Invalid index", "The index does not exist"
        
    End If
        
End Property


Public Property Set Item(ByVal ipIndex As Long, ByVal ipValue As clsNewSector)

    ' Avoid creating a dictionary entry by assignment
    If s.Host.exists(ipIndex) Then
    
        Set s.Host.Item(ipIndex) = ipValue
        
    Else
    
        Err.Raise 381, "Invalid index", "The index does not exist"
        
    End If
        
End Property

Public Sub Add(ByVal ipKey As Long, ByVal ipItem As clsNewSector)

    If s.Host.exists(ipKey) Then
    
        Err.Raise 457, "Key error", "A value is already associated with the key"
        
    End If
    
    s.Host.Add ipKey, ipItem
    
End Sub

Public Function Keys() As Variant

    Keys = s.Host.Keys
    
End Function

Public Function Items() As Variant

    Items = s.Host.Items
    
End Function

OP 原代码现在可以替换为

Private pSectors As clsNewSectors
Set pSectors = new clsNewSectors

上面的这个想法可以扩展以增加更多的灵活性(例如用于多种类型对象的工厂),但这是一个单独的讨论。

上面的代码编译正常,不会生成任何有意义的 RUbberduck 检查。

上面的代码需要引用 Microsoft Scripting Runtime。

【讨论】:

  • Dictionary vs Collection 有优势吗?
  • @BrianMStafford 好问题。如果您不需要访问或迭代密钥,Collection 可以很好地包装到自定义集合类中。
  • Scripting.Dictionary 的优势在于“exists”方法以及您可以通过 Keys 和 Items 方法检索 Keys 或 Items 数组的事实。例如,如果您将数据推回 Excel,这将非常有用。如果您不需要这些设施,那么 Collection 就可以了。在我上面提供的代码中,我忘记添加 count 函数并且存在于包装类中。
【解决方案2】:

数组初始化:

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 接口,调用代码现在负责告诉函数用什么来初始化数组,只需负责为工厂提供实现即可。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-31
    • 1970-01-01
    相关资源
    最近更新 更多