Custom Enumerators
A class becomes usable with For Each when one of its members supplies an enumerator: an object that implements the COM interface IEnumVARIANT. The member is marked with the [Enumerator] attribute. It can return the enumerator of an object that already has one, such as a Collection, or an enumerator the class implements itself. Writing an enumerator shows a class that produces its own items, with no collection behind it.
Returning a Collection’s enumerator
A class that keeps its items in a Collection returns the collection’s own enumerator, which is its hidden _NewEnum member. The square brackets let the name start with an underscore.
Class Bag
Private Items As New Collection
Public Sub Add(ByVal Item As Variant)
Items.Add Item
End Sub
[Enumerator]
Public Function _NewEnum() As stdole.IUnknown
Return Items.[_NewEnum]
End Function
End Class
Dim b As New Bag
b.Add "a"
b.Add "b"
b.Add "c"
Dim x As Variant
For Each x In b
Debug.Print x
Next
' Output:
' a
' b
' c
Each For Each calls the member again and gets an enumerator of its own, so two loops over the same object — one nested in the other — do not disturb each other.
The enumerator member
- The attribute is what counts. Mark the member with [Enumerator], or with [DispId(-4)], the dispatch identifier COM reserves for it. A member named
_NewEnumwithout either attribute is not used: For Each raises error 438, Object doesn’t support this property or method, as it does for a class with no enumerator at all. - The name and the visibility are free.
_NewEnumis the convention, but a member calledItemsworks too, and so does a Private or Friend one. - It can be a Function or a Property Get. It returns stdole.IUnknown, a Variant holding the enumerator, or the type of a class that implements IEnumVARIANT.
Note
twinBASIC also accepts a member that returns the Collection itself rather than its [_NewEnum], and For Each then uses the collection’s enumerator. VB6 does not: For Each raises error 451, Property let procedure not defined and property get procedure did not return an object. Return [_NewEnum] in code that must also run in VB6.
Writing an enumerator
When the items are not in a collection — a range of numbers, records read on demand — the class implements IEnumVARIANT itself. The interface in stdole cannot be implemented from twinBASIC: its counts are unsigned 32-bit integers, which have no twinBASIC type, so the compiler reports {unsupported VT_UI4} for them. Declare the interface in the project instead, with the same interface identifier and Long counts:
[InterfaceId("00020404-0000-0000-C000-000000000046")]
Private Interface IEnumVARIANT Extends stdole.IUnknown
Sub Next(ByVal celt As Long, ByRef rgVar As Variant, ByRef pCeltFetched As Long)
Sub Skip(ByVal celt As Long)
Sub Reset()
Function Clone() As IEnumVARIANT
End Interface
For Each uses the enumerator in a fixed way, and an implementation has to allow for it:
- It calls Reset first, then Next once per item, always with celt = 1. Skip and Clone are never called, so they can return
E_NOTIMPL. - It passes pCeltFetched as a null pointer. Assigning to it then fails with an access violation, so test its address with VarPtr first.
- Next ends the loop by returning
S_FALSE(1) through Err.ReturnHResult. Without it the loop never ends: For Each cannot see the count of 0, and calls Next again for as long as it returnsS_OK. An item written together withS_FALSEis dropped, so the call that writes the last item returnsS_OK, and the call after itS_FALSE. - An error raised in Next ends the loop and reaches the error handler of the procedure running the For Each, with its number and description.
Class RangeEnum
Implements IEnumVARIANT
Private Const S_FALSE As Long = 1
Private Const E_NOTIMPL As Long = &H80004001
Private First As Long, Last As Long, Current As Long
Public Sub Init(ByVal FromValue As Long, ByVal ToValue As Long)
First = FromValue
Last = ToValue
Current = FromValue
End Sub
Private Sub Next(ByVal celt As Long, ByRef rgVar As Variant, ByRef pCeltFetched As Long) _
Implements IEnumVARIANT.Next
If Current > Last Then
If VarPtr(pCeltFetched) <> 0 Then pCeltFetched = 0
Err.ReturnHResult = S_FALSE
Exit Sub
End If
rgVar = Current
Current += 1
If VarPtr(pCeltFetched) <> 0 Then pCeltFetched = 1
End Sub
Private Sub Skip(ByVal celt As Long) Implements IEnumVARIANT.Skip
Err.ReturnHResult = E_NOTIMPL
End Sub
Private Sub Reset() Implements IEnumVARIANT.Reset
Current = First
End Sub
Private Function Clone() As IEnumVARIANT Implements IEnumVARIANT.Clone
Err.ReturnHResult = E_NOTIMPL
End Function
End Class
Class NumberRange
Private First As Long, Last As Long
Public Sub Init(ByVal FromValue As Long, ByVal ToValue As Long)
First = FromValue
Last = ToValue
End Sub
[Enumerator]
Public Function _NewEnum() As stdole.IUnknown
Dim e As New RangeEnum
e.Init First, Last
Return e
End Function
End Class
Dim r As New NumberRange
r.Init 3, 5
Dim n As Variant
For Each n In r
Debug.Print n
Next
' Output:
' 3
' 4
' 5
This Next returns one item per call, which is all For Each asks for. Native code that calls the enumerator directly may ask for several items at once, and an enumerator meant for such callers must handle a celt above 1. IEnumVARIANT describes the whole contract, the declaration that allows it, and an enumerator that implements it.
See Also
- For Each…Next statement
- Enumerator attribute
- IEnumVARIANT interface
- Interfaces and CoClasses – declaring an interface in twinBASIC
- Implements statement
- Collection class