patternMinor
More imitation of Enumerable in VBA
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
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
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
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
But before you can check for a reference, you need to be sure you're dealing with an
vba gives us the
This splits the function in two distinct parts: the
What you're trying to do here, is to perform an equality comparison. When
Bottom line: don't mix apples and oranges - a
Here's how I had implemented
Well ...yeah, so here's how I had implemented
Where
For completeness, the interesting part:
Custom classes could implement a simple
And vba has a
(again, that one only checks the first item 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
NextWhat 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 IfBottom 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 FunctionWell ...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 FunctionWhere
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 FunctionCustom 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 FunctionAnd 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
NextIf ObjPtr(item) = ObjPtr(itemToSearchFor) Then
Contains = True
Exit Function
End IfPublic Function Contains(value As Variant) As Boolean
'Determines whether an element is in the List.
Contains = (IndexOf(value) <> -1)
End FunctionPublic 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 FunctionPrivate 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 FunctionContext
StackExchange Code Review Q#59595, answer score: 4
Revisions (0)
No revisions yet.