HiveBrain v1.2.0
Get Started
← Back to all entries
patternMajor

List<T> implementation for VB6/VBA

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
implementationvb6forvbalist

Problem

Recently I decided VB6's Collection wasn't enough for my needs, so I decided to implement something like C#'s List. Here's the class that resulted, I'd like to know if the implementation could be made better /more efficient, especially with Insert and Sort methods; also I'd like another pair of eyes to examine the errors being raised and see if it all makes sense - the idea isn't to throw every error that's possible to get with a List, but I might have missed throwing an error that could help usability.

I've been using this List class in VB6 code for a little less than a week now, and seriously, it's like the best thing since sliced bread - being able to add items inline is awesome, and all those members make Collection look awfully boring and make me want to implement a keyed version, which I'm guessing could wrap a Dictionary instead.

Class definition and private functions

As with all classes I write, I start with declaring a Private Type that defines what the class encapsulates, and then I make a private instance of that type which I call this and in the rest of the code I refer to this, which does not have the same meaning as Me (Me refers to the current instance of the List class, while this refers to the encapsulated stuff - as you'll notice I only use Me when I have to).

I have a debate with myself as to whether the RaiseErrorXXXX procedures should be made public or not - doing so would document the errors thrown at the API level, but wouldn't serve any real purpose.

`Attribute VB_Name = "List"
Private Type tList
Encapsulated As Collection
ItemTypeName As String
End Type

Private this As tList
Option Explicit

Private Function IsReferenceType() As Boolean
If Count = 0 Then Exit Function
IsReferenceType = IsObject(this.Encapsulated(1))
End Function

Private Function IsComparable() As Boolean
If IsReferenceType Then
IsComparable = TypeOf First Is IComparable
End If
End Function

Private Fun

Solution

OK, went through the code and gave this a bit of thought over the past couple of days. As far as the implementation goes, I don't see a whole lot that I would change (that you didn't identify in the answer above) other than a couple nit-picky things. First, is the use of the this variable identifier. I couldn't find anything that justifies the naming and data structure other than imitating a .NET keyword. The Me keyword (as ridiculous as it sounds after writing C# for a while) is obvious to a VB6 programmer - this is not. I would personally stick with individual member variables instead of the Type, but if using the Type I would name it something like memberData. The fact that you were compelled to explain in the post what this refers to in the class is a red flag because it isn't immediately obvious.

The second nit-pick is also related to using .NET metaphors that do not directly map to a VB6 context, but this one comes from the opposite direction (and falls into the "errors being raised" category). A .NET programmer will expect assignments that are not type-safe to fail at compile time, not runtime. For example, this snippet in VB6 compiles and runs without complaint:

Dim first as Variant
Dim second as Variant

first = "String"
second = 1234

first = second

'First is now an integer.
Debug.Print(TypeName(first))


The analogous code in C# doesn't:

var first = "String";
var second = 1234;

//This fails due to implicit conversion:
first = second;


So if the intention is to enforce type safety, the better meta solutions would be to not use Variant types if they can be avoided or to use an IDE plug-in to make sure your assignments are type safe. If the intention is to simply replicate the functionality of the .NET List object, this is an entirely different matter (and one that is both useful and well executed, BTW).

Nit-picking aside, let's get down to the "better /more efficient" side of things. Given that the Collection object in VB6 isn't much more than a glorified array (4 methods - seriously?), I would just skip it entirely and just wrap an array directly. The vast majority of the class just uses the Collection for storage space, and the fact that the intention is to ensure strong typing makes the memory management a lot easier. Note that I am not recommending an array of Variants, and am at risk of getting into StackOverflow territory.

VB6 is based on Window COM, and uses a SAFEARRAY structure to store all of its arrays internally. This (vastly simplified) defines the size of each array element, tracks the number of elements in each dimension, stores a couple COM flags, and holds a pointer to the array data. Since VB6 is COM based, it also has a couple of undocumented functions like for pointer resolution and manipulation and can directly access the Windows API. This gives you the ability to do inserts and deletes into the middle of arrays with memory copy operations instead of iterating over the array or the a Collection.

You can get the underlying data structure like this:

Private Const VT_BY_REF = &H4000&

Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (pDst As Any, pSrc As Any, ByVal ByteLen As Long)

'STRUCTS FOR THE SAFEARRAY:
Private Type SafeBound
    cElements As Long
    lLbound As Long
End Type

Private Type SafeArray
    cDim As Integer
    fFeature As Integer
    cbElements As Long
    cLocks As Long
    pvData As Long
    rgsabound As SafeBound
End Type

Private Function GetArrayInfo(vArray As Variant, uInfo As SafeArray) As Boolean

    'NOTE, the array is passed as a variant so we can get it's absolute memory address.  This function
    'loads a copy of the SafeArray structure into the UDT.

    Dim lPointer As Long, iVType As Integer

    If Not IsArray(vArray) Then Exit Function               

    With uInfo
        CopyMemory iVType, vArray, 2                        'First 2 bytes are the subtype.
        CopyMemory lPointer, ByVal VarPtr(vArray) + 8, 4    'Get the pointer.

        If (iVType And VT_BY_REF) <> 0 Then                 'Test for subtype "pointer"
            CopyMemory lPointer, ByVal lPointer, 4          'Get the real address.
        End If

        CopyMemory uInfo.cDim, ByVal lPointer, 16           'Write the safearray to the passed UDT.

        If uInfo.cDim = 1 Then                              'Can't do multi-dimensional
            CopyMemory .rgsabound, ByVal lPointer + 16, LenB(.rgsabound)
            GetArrayInfo = True
        End If
    End With

End Function


The beauty of this approach is that because you now have the underlying data structure of the array in a variable, you can just change the pvData pointer to any memory that has been allocated, and set cbElements to the SizeOf() the data type of the list. An Add() function is then just shifting memory one element higher in memory from your insert offset and dropping in the new item, and Remove() is the opposite

Code Snippets

Dim first as Variant
Dim second as Variant

first = "String"
second = 1234

first = second

'First is now an integer.
Debug.Print(TypeName(first))
var first = "String";
var second = 1234;

//This fails due to implicit conversion:
first = second;
Private Const VT_BY_REF = &H4000&

Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (pDst As Any, pSrc As Any, ByVal ByteLen As Long)

'STRUCTS FOR THE SAFEARRAY:
Private Type SafeBound
    cElements As Long
    lLbound As Long
End Type

Private Type SafeArray
    cDim As Integer
    fFeature As Integer
    cbElements As Long
    cLocks As Long
    pvData As Long
    rgsabound As SafeBound
End Type

Private Function GetArrayInfo(vArray As Variant, uInfo As SafeArray) As Boolean

    'NOTE, the array is passed as a variant so we can get it's absolute memory address.  This function
    'loads a copy of the SafeArray structure into the UDT.

    Dim lPointer As Long, iVType As Integer

    If Not IsArray(vArray) Then Exit Function               

    With uInfo
        CopyMemory iVType, vArray, 2                        'First 2 bytes are the subtype.
        CopyMemory lPointer, ByVal VarPtr(vArray) + 8, 4    'Get the pointer.

        If (iVType And VT_BY_REF) <> 0 Then                 'Test for subtype "pointer"
            CopyMemory lPointer, ByVal lPointer, 4          'Get the real address.
        End If

        CopyMemory uInfo.cDim, ByVal lPointer, 16           'Write the safearray to the passed UDT.

        If uInfo.cDim = 1 Then                              'Can't do multi-dimensional
            CopyMemory .rgsabound, ByVal lPointer + 16, LenB(.rgsabound)
            GetArrayInfo = True
        End If
    End With

End Function

Context

StackExchange Code Review Q#32618, answer score: 31

Revisions (0)

No revisions yet.