



If you implement a custom container or collection class, you almost
certainly want to be able to enumerate or iterate its members.
Ordinarily, this is done using the For Each...Next statement.
However, attempting to simply define a custom collection class
without providing some means for For Each...Next to operate
results in a compiler error (error BC32023, "Expression is of
type 'xxxx', which is not a collection type.").
So For Each...Next support is not automatic; in order to use
the construct to iterate the members stored in your container, you
must take explicit steps to make your container enumerable.
One choice is to inherit from a collection class that itself implements
support for the For Each...Next construct. Unfortunately, you
can't inherit from the Visual Basic Collection class, since it's
marked as non-inheritable. Instead, you can inherit from one of the
collection classes in System.Collections or
System.Collections.Specialized. The following code, for
example, implements a custom container that derives from the
DictionaryBase class:
Imports Microsoft.VisualBasic
Imports System
Imports System.Collections
Public Class MyCollection
Inherits System.Collections.DictionaryBase
Public Sub Add(key As [String], value As [String])
Dictionary.Add(key, value)
End Sub
End Class
Public Class UseMyCollection
Public Shared Sub Main()
Dim myC As New MyCollection
myC.Add("NY", "New York")
myC.Add("CA", "California")
myC.Add("MI", "Michigan")
For Each d As DictionaryEntry in MyC
Console.WriteLine(d.Key & ": " & d.Value)
Next
End Sub
End Class
When the program is compiled and run, it produces the following output:
CA: California
NY: New York
MI: Michigan
Because we've inherited from a class that supports enumeration, we can use
For Each...Next "for free," without having to do anything special.
If none of the existing container classes quite meet our needs, our second
choice is to build a container class from scratch. In this case, we must explicitly
implement support for For Each...Next. To do this, our custom class
must support two interfaces: IEnumerable and IEnumerator.
IEnumerable has a single method:
Function GetEnumerator() As IEnumerator
The IEnumerator type returned by
IEnumerable.GetEnumerator has the following three members:
' Returns the Current Data Item
ReadOnly Property Current As Object
' Returns True if the enumerator moved
' successfully, false otherwise
Function MoveNext() As Boolean
' Resets the enumerator to point to one
' position before the first element
Sub Reset()
In other words, our custom class must implement a GetEnumerator
method that returns an IEnumerable interface. In implementing the latter
interface, we must explicitly position and move the emumerator for each
iteration of the For Each...Next loop. As long as we properly
implement these members, For Each...Next will handle the mechanics
of calling the appropriate method.
The following code shows a simple implementation of a custom container that
can be enumerated using For Each...Next:
Imports Microsoft.VisualBasic
Imports System
Imports System.Collections
Public Class NameContainer
Implements IEnumerator
Implements IEnumerable
Dim iCurrent As Integer = 0
Dim sName(10) As String
Dim ptr As Integer = -1
Dim upperbnd As Integer = 0
Public Sub Add(Value As String)
If upperbnd > UBound(sName) Then
ReDim Preserve sName(Ubound(sName) + 10)
End If
sName(upperbnd) = Value
upperbnd += 1
End Sub
Public ReadOnly Property Current() As Object _
Implements IEnumerator.Current
Get
Return sName(ptr)
End Get
End Property
Public Function MoveNext() As Boolean _
Implements IEnumerator.MoveNext
ptr += 1
If ptr < upperbnd Then Return True
End Function
Public Sub Reset() _
Implements IEnumerator.Reset
ptr = -1
End Sub
Public Function GetEnumerator() As IEnumerator _
Implements IEnumerable.GetEnumerator
Return Me
End Function
End Class
Public Class TestCollec
Public Shared Sub Main()
Dim na As New NameContainer
na.Add("Alaskan Malamute")
na.Add("Siberian Husky")
na.Add("Keeshound")
na.Add("Chinook")
For Each s As String In na
Console.WriteLine(s)
Next
End Sub
End Class
Notice that the IEnumerator interface works by defining a record pointer
to a particular element of an array or collection. When the enumerator is
created by the call to the GetEnumerator method, MoveNext is called
automatically to position the pointer to the first element of the collection.
Hence, the pointer must be initialized to point to one position before the first
element of the array or collection. In the case of a zero-based array, this means
the pointer must be initialized to have a value of -1.
It's also interesting to note that we can enumerate the members of a class
without using For Each...Next as follows:
Dim iArr() As Integer = {1,2,3,4,5}
Dim iEnum As IEnumerator = iArr.GetEnumerator()
Do While iEnum.MoveNext
Console.WriteLine(iEnum.Current)
Loop
Interestingly, this is precisely the code generated by the compiler when it
encounters the For Each...Next construct, as the following output
from ILDasm shows:
IL_001e: callvirt instance class
[mscorlib]System.Collections.IEnumerator
[mscorlib]System.Array::GetEnumerator()
IL_0023: stloc.1
IL_0024: br.s IL_0036
IL_0026: ldloc.1
IL_0027: callvirt instance object
[mscorlib]System.Collections.IEnumerator::get_Current()
IL_002c: call object
[mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::G
etObjectValue(object)
IL_0031: call void
[mscorlib]System.Console::WriteLine(object)
IL_0036: ldloc.1
IL_0037: callvirt instance bool
[mscorlib]System.Collections.IEnumerator::MoveNext()

Howling Wolf Consulting Services
Implementing an Enumerable Container with Visual Basic .NET
|