Determine the type of an uninitialized array in VBA

Viewed 311

I want to write a function that, when given a declared but uninitialized array for a user-defined class, the function will identify the type of objects the array is designed to hold, and then create new objects for the array. I have a class called "clsNewSector", and I declare an array in another class like this:

Private pSectors(1 To 3) As clsNewSector

Then I have a function that should be able to identify that although every object references "Nothing" at the moment, it should be able to get the type of the array, which in this case is clsNewSector, but in other cases the class type might change.

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

The typename function does not work because it just returns "Nothing" since the array is uninitialized, it is unable to identify that the variable was declared as a different type. Is there any way to find the declared type of an uninitialized array so that I can then use a for loop to initialize each member of the array with the correct type of new object.

2 Answers

The question the OP is asking is essentially about initialising an object which is (in this case) a collection of clsNewSector.

An easy way to do this is to wrap a scripting.dictionary in a strongly types Class.

The code below is for an object called clsNewSectors which will create an object containing a scripting dictionary holding three clsNewSector objects.

The Item property of the scripting dictionary is strongly typed so that we can only add, set or get clsNewSector objects.

If you examine the code below you can see it can be repurposed for different objects by doing a search and replace on ClsNewSector only (assuming an integer long key is still acceptable)

Class 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

The OP original code can now be replaced with

Private pSectors As clsNewSectors
Set pSectors = new clsNewSectors

This idea above can be extended to add more flexibility (such as a factory for multiple types of object) but that's a separate discussion.

The code above compiles OK and does not generate any RUbberduck inspections of significance.

The code above requires a reference to the Microsoft Scripting RUntime.

The array is initialized:

Private pSectors(1 To 3) As clsNewSector

Its individual elements aren't assigned, but the array itself is ready for whatever you want to throw at it: it's reserving 3 spots for clsNewSector instances, at compile-time. Being an object type, the array is merely holding pointers. Under the hood, VBA arrays point to a SAFEARRAY structure that look something like this:

typedef struct tagSAFEARRAY {
  USHORT         cDims;
  USHORT         fFeatures;
  ULONG          cbElements;
  ULONG          cLocks;
  PVOID          pvData;
  SAFEARRAYBOUND rgsabound[1];
} SAFEARRAY;

The type metadata for any given element is only accessible via a series of successive pointer reads; the array structure itself is abstracting away the concept of a type of element, such that it's capable of supporting any type. This content (not mine) goes into further details.

Whether a programming language might let a Foo into an array of Bar objects is all about type safety, which . In .NET (and other techs), type safety is very extensively explored, with generics and other advanced mechanisms. In VBA, we're 25 years ago, with a compiler that defers a lot of type safety to last-minute runtime checks - this code compiles perfectly fine:

Public Sub Test()
    Dim things(1 To 10) As Class1
    Set things(1) = New Class2 '<~ *run-time* error 13 "type mismatch"
End Sub

Now, if there's some level of run-time type safety, surely that means the type metadata exists somewhere! And it does - but VBA does not have an extensive type system surfaced to the language, and much of its internal mechanics aren't [easily] programmatically accessible (call stack comes to mind).

In any function call, the arguments are evaluated first - so when we do TypeName(a(1)), the a(1) subscript resolves to a NULL object pointer (no reference was Set yet), so Nothing is passed to the TypeName function, which returns the string "Nothing" since VBA has pretty weird typing semantics in this area - but if we leave the type of individual elements alone for a minute and look at the data type of the array itself...

Public Sub Test()
    Dim things(1 To 10) As Class1
    Debug.Print TypeName(things) ' prints "Object()"
End Sub

The fact that the reported data type of our explicitly-typed array is Object() tells us everything we need to know: the real type metadata of the array itself is being abstracted away, so there is no [simple?] VBA code that can be given an array and an object variable, and determine whether the object is of the correct type for the array, unless we hard-code the expected data type and don't care for null references.

The problem we're facing here, is that the reason we need to determine a specific data type is to spawn a new object of that type - and since object references initialize to Nothing, that's the only "data type" we'll only get.

Unless we initialize the array differently.

You could make clsNewSector have a default instance (set its VB_PredeclaredId attribute value to True, or just use and synchronize Rubberduck's @PredeclaredId annotation, and then expose a property like these:

Public Property Get IsDefault() As Boolean
    IsDefault = Me Is clsNewSector
End Property

And then you could initialize pSectors with pointers to the class' default instance as soon as possible:

For i = LBound(pSectors) To UBound(pSectors)
    Set pSectors(i) = clsNewSector
Next

That way no subscript would default to Nothing when you pass a(1) to TypeName, and then the rest of the code is cursed with having to verify whether the instance they're looking at has IsDefault = True, otherwise we run the risk of making that global default instance stateful, and we wouldn't want that to happen (shared global state at various array indices - what could possibly go wrong?!).

Overall pretty frail, definitely not ideal.

An idiomatic solution would be to create a custom collection class per type, that gets to enforce type safety on its own terms. Could be a NewSectors class (consider dropping that cls prefix) whose Item indexed Property Set member will throw an error given Nothing or any object reference that isn't a NewSector instance.


The root of the problem is the too many apparent responsibilities of the NewObject function, which seemingly can create anything given a string with a type name.

Let's think outside the box for a minute.

Function InitializeObjectArray(a As Variant) As Variant

We use the array a itself to get the name of a type, and then we pass that name to a function that gets us the object of the type we expect.

So the function is inherently coupled with NewObject, whose name isn't telling us much about the breadth of its applicability.

This looks like a job for...

What if we moved that responsibility to a formalized interface?

@Interface IArrayItemFactory
Option Explicit

Public Function Create() As Object
End Function

Then we could have a NewSectorFactory class that implements it as follows:

Implements IArrayItemFactory
Option Explicit

Private Function IArrayItemFactory_Create() As Object
    Set IArrayItemFactory_Create = New clsNewSector
End Function

Now InitializeObjectArray might look like this:

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

By stripping the function of the responsibility of working out the type of object to create, and moving it into its own object/dependency, the function is now easily able to initialize any array of any object type that has an implementation for the IArrayItemFactory interface, and the calling code is now responsible for telling the function what to initialize the array with, by simply being responsible for providing the implementation for the factory.

Related