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

More imitation of Enumerable in VBA

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

Problem

I was inspired by Me How's question to see how far I could push an imitation of .Net's Enumerable Class.

The new functions can obviously handle Collections, but can also handle any collection-type object whose items have a default value. If a collectionObject's items don't have a default value, Runtime Error 438 "Object does not support property or method" is raised. So, things like Cells and Range work, but Worksheets doesn't.

Min and Max only differ by one operator, so there's some duplication there, but I don't know how to refactor it out. Intersect also seems inefficient to me, but I couldn't imagine a better algorithm. So, those are particular areas of interest to me. Suggestions for which features to add next would also be appreciated.
Enumerable.cls

```
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "Enumerable"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False

Private c As Collection ' used for Range

Public Function Range(ByVal startValue As Long, ByVal endValue As Long) As Collection
Attribute Range.VB_Description = "Returns a collection of longs."
Set c = New Collection
Dim i As Long
For i = startValue To endValue
c.Add i
Next
Set Range = c
End Function

Public Property Get NewEnum() As IUnknown
Attribute NewEnum.VB_UserMemId = -4
Set NewEnum = c.[_NewEnum]
End Property

' All of these functions work only on collectionObjects whose items have a default value.
' If the items do not have a default value,
' Runtime Error 438 "Object doesn't support this property or method" is raised.

Public Function Contains(collectionObject As Variant, itemToSearchFor As Variant) As Boolean
Attribute Contains.VB_Description = "Checks if an item exists in a Collection. Matches on the default property."
Dim item As Variant

For Each item In collectionObject
If item = itemToSearchFor Then
C

Solution

I don't like that you're relying on default properties, especially in the Contains method: in many cases your Contains method will return false positives for objects.

Simply because an object has value XYZ for its default property doesn't mean another object with value XYZ can be considered equal - in fact, with your code two objects of different types could be considered equal under some unfortunate circumstances.

Fortunately that's an easily avoidable bug.

A collection of objects will hold a bunch of references - to know if a given object exists in a collection you can iterate the collection and if you find that reference, you can safely return True and be confident that the specified object does exist in the collection.

But before you can check for a reference, you need to be sure you're dealing with an Object reference, because ObjPtr(1234) (or any other non-object value) will raise a "Type Mismatch" runtime error.

vba gives us the IsObject function to do that. Hence, first thing I'd do is try to know whether I'm dealing with an object or not.

This splits the function in two distinct parts: the Else block can deal with what I like referring to as value types - basically anything that's not an Object. Your code works well in these cases:

Public Function Contains(collectionObject As Variant, itemToSearchFor As Variant) As Boolean
    Dim item As Variant
    For Each item In collectionObject

        If IsObject(item) Then

        Else

            If item = itemToSearchFor Then
                Contains = True
                Exit Function
            End If

        End If

    Next


What you're trying to do here, is to perform an equality comparison. When item is an Object the only way to reliably determine if the itemToSearchFor is the same object as your item, is to compare their references:

If ObjPtr(item) = ObjPtr(itemToSearchFor) Then
    Contains = True
    Exit Function
End If


Bottom line: don't mix apples and oranges - a Variant can be anything, and since a Collection can be a bunch of totally unrelated things of various types, you have no choice but to evaluate IsObject for each item.

Here's how I had implemented Contains in my List implementation:

Public Function Contains(value As Variant) As Boolean
'Determines whether an element is in the List.

    Contains = (IndexOf(value) <> -1)

End Function


Well ...yeah, so here's how I had implemented IndexOf in that class:

Public Function IndexOf(value As Variant) As Long
'Searches for the specified object and returns the 1-based index of the first occurrence within the entire List.

    Dim found As Boolean
    Dim isRef As Boolean
    isRef = IsReferenceType

    Dim i As Long

    If Count = 0 Then IndexOf = -1: Exit Function
    For i = 1 To Count

        If isRef Then

            found = EquateReferenceTypes(value, item(i))

        Else

            found = EquateValueTypes(value, item(i))

        End If

        If found Then IndexOf = i: Exit Function

    Next

    IndexOf = -1

End Function


Where IsReferenceType calls IsObject on the first item - in that case I could safely only check the first item in the list, because that collection could only ever have items of a single type, so if the first item was an object, they're all objects.

For completeness, the interesting part:

Private Function EquateValueTypes(value As Variant, other As Variant) As Boolean

    EquateValueTypes = (value = other)

End Function

Private Function EquateReferenceTypes(value As Variant, other As Variant) As Boolean

    Dim equatable As IEquatable
    If IsEquatable Then

        Set equatable = value
        EquateReferenceTypes = equatable.Equals(other)

    Else

        'object doesn't implement IEquatable - use reference equality:
        EquateReferenceTypes = (ObjPtr(value) = ObjPtr(other))

    End If

End Function


Custom classes could implement a simple IEquatable interface if reference equality wasn't desirable:

Public Function Equals(ByVal other As Variant) As Boolean
'return True if [other] can be considered equal to this instance.
End Function


And vba has a TypeOf keyword that can be used for checking types, so you could have a little function like this, to call before attempting a "type cast":

Private Function IsEquatable() As Boolean
    If IsReferenceType Then
        IsEquatable = TypeOf First Is IEquatable
    End If
End Function


(again, that one only checks the first item in the List - it's just food for thought ;)

Code Snippets

Public Function Contains(collectionObject As Variant, itemToSearchFor As Variant) As Boolean
    Dim item As Variant
    For Each item In collectionObject

        If IsObject(item) Then

        Else

            If item = itemToSearchFor Then
                Contains = True
                Exit Function
            End If

        End If

    Next
If ObjPtr(item) = ObjPtr(itemToSearchFor) Then
    Contains = True
    Exit Function
End If
Public Function Contains(value As Variant) As Boolean
'Determines whether an element is in the List.

    Contains = (IndexOf(value) <> -1)

End Function
Public Function IndexOf(value As Variant) As Long
'Searches for the specified object and returns the 1-based index of the first occurrence within the entire List.

    Dim found As Boolean
    Dim isRef As Boolean
    isRef = IsReferenceType

    Dim i As Long

    If Count = 0 Then IndexOf = -1: Exit Function
    For i = 1 To Count

        If isRef Then

            found = EquateReferenceTypes(value, item(i))

        Else

            found = EquateValueTypes(value, item(i))

        End If

        If found Then IndexOf = i: Exit Function

    Next

    IndexOf = -1

End Function
Private Function EquateValueTypes(value As Variant, other As Variant) As Boolean

    EquateValueTypes = (value = other)

End Function

Private Function EquateReferenceTypes(value As Variant, other As Variant) As Boolean

    Dim equatable As IEquatable
    If IsEquatable Then

        Set equatable = value
        EquateReferenceTypes = equatable.Equals(other)

    Else

        'object doesn't implement IEquatable - use reference equality:
        EquateReferenceTypes = (ObjPtr(value) = ObjPtr(other))

    End If

End Function

Context

StackExchange Code Review Q#59595, answer score: 4

Revisions (0)

No revisions yet.