Introduction
I like to use the enumerators features of .NET Framework, specially the foreach
keyword. The code looks really clean and is easy to understand and maintain. But enumerators lack several desirable features like:
- Circularity
- Reversibility
- Constraining
- Stepping
I have created a helper class that enhances enumeration of existing collection enumerators. Since there are lots of collections in .NET's System.Collections
and System.Collections.Generic
namespaces, with proper enumerators, even a developer can create his/her own collection classes with custom enumerators, the solution must point to modify the behaviour of those enumerators instead of creating custom enumerators for each kind of collection.
The Helper Class
The helper class wraps a set of static
methods that can be used without instantiating an object. They are defined in the System.Collections
namespaces, so you never have to worry about using an extra namespace in your source file.
namespace System.Collections
{
public class Enumerators
{
public static IEnumerable CircularEnum(IEnumerable _enumerator) ...
public static IEnumerable ReverseEnum(IEnumerable _enumerator) ...
public static IEnumerable SteppedEnum(IEnumerable _enumerator, int _step) ...
public static IEnumerable ConstrainedEnum
(IEnumerable _enumerator, int _start) ...
public static IEnumerable ConstrainedEnum
(IEnumerable _enumerator, int _start, int _count) ...
}
}
The Sample Collection
As can be read in the previous code, all enumerator methods will receive another enumerator object (any derived from IEnumerable
interface). For testing purposes, I have included a SortedList
generic collection, that can be enumerated in key/value pairs or individually by key or value lists.
SortedList<int,string> list = new SortedList<int,string>();
list.Add(1, "one");
list.Add(2, "two");
list.Add(3, "three");
list.Add(4, "four");
list.Add(5, "five");
list.Add(6, "six");
list.Add(7, "seven");
list.Add(8, "eight");
list.Add(9, "nine");
list.Add(10, "ten");
Circular Enumerator
The simplest enumerator is the circular, processed by the CircularEnum
method; it will invoke the original enumerator inside an infinite while
loop, as follows:
public static IEnumerable CircularEnum(IEnumerable _enumerator)
{
while (true)
{
IEnumerator enu = _enumerator.GetEnumerator();
while (enu.MoveNext())
{
yield return enu.Current;
}
}
}
There should be some kind of control to stop the infinite loop under certain condition. In the sample code, there is a constant defined as:
const int max = 15;
Having defined a stop behaviour, the circular enumerator can be used as:
Console.WriteLine("Dictionary circular enumeration:");
int i = 0;
foreach (KeyValuePair<int, string> pair in Enumerators.CircularEnum(list))
{
Console.WriteLine(" " + pair.ToString());
if (++i >= max)
break;
}
Console.WriteLine(" (stopped)\r\n");
OUTPUT
Dictionary circular enumeration:
[1, one]
[2, two]
[3, three]
[4, four]
[5, five]
[6, six]
[7, seven]
[8, eight]
[9, nine]
[10, ten]
[1, one]
[2, two]
[3, three]
[4, four]
[5, five]
(stopped)
Constrained Enumerator
Sometimes it is needed to traverse just a portion of an enumerated collection, something like the Array.Copy()
method. The ConstrainedEnum
method has two versions to allow specify a starting element and optionally an element count. First element has zero index, and count can be zero or greater.
public static IEnumerable ConstrainedEnum(IEnumerable _enumerator, int _start)
{
if (_start < 0)
throw new ArgumentException
("Invalid step value, must be positive or zero.");
IEnumerator enu = _enumerator.GetEnumerator();
while (enu.MoveNext())
{
if (--_start < 0)
yield return enu.Current;
}
}
public static IEnumerable ConstrainedEnum
(IEnumerable _enumerator, int _start, int _count)
{
if (_start < 0)
throw new ArgumentException
("Invalid step value, must be positive or zero.");
if (_count < 0)
throw new ArgumentException
("Invalid count value, must be positive or zero.");
if (_count > 0)
{
IEnumerator enu = _enumerator.GetEnumerator();
if (enu.MoveNext())
{
while (--_start > 0)
{
if (!enu.MoveNext())
break;
}
if (_start <= 0)
{
while (--_count >= 0)
{
if (enu.MoveNext())
yield return enu.Current;
else
break;
}
}
}
}
}
The sample code uses the second version of the ConstrainedEnum
method:
Console.WriteLine("Constrained enumeration (2,5):");
foreach (KeyValuePair<int,> pair in Enumerators.ConstrainedEnum(list, 2, 5))
{
Console.WriteLine(" " + pair.ToString());
}
Console.WriteLine(" (finished)\r\n");
OUTPUT
Constrained enumeration (2,5):
[3, three]
[4, four]
[5, five]
[6, six]
[7, seven]
(finished)
Stepped Enumerator
This method (SteppedEnum
) allows to traverse a collection skipping some elements. Using a step value 1 will behave like a regular enumerator, step value 2 will skip one element in every iteration, etc.
public static IEnumerable SteppedEnum(IEnumerable _enumerator, int _step)
{
if (_step < 1)
throw new ArgumentException
("Invalid step value, must be greater than zero.");
IEnumerator enu = _enumerator.GetEnumerator();
while (enu.MoveNext())
{
yield return enu.Current;
for (int i = _step; i > 1; i--)
if (!enu.MoveNext())
break;
}
}
The sample code will enumerate a collection skipping two elements in each iteration:
Console.WriteLine("Stepped enumeration (3):");
foreach (int value in Enumerators.SteppedEnum(list.Keys, 3))
{
Console.WriteLine(" " + value.ToString());
}
Console.WriteLine(" (finished)\r\n");
OUTPUT
Stepped enumeration (3):
1
4
7
10
(finished)
Bonus: Reverse Enumerator
.NET enumerators (derived from IEnumerable
) are not designed to be traversed backwards, then, in theory, there cannot be a reverse enumerator. The ReverseEnum
method does some reflection processing to find an indexer property in the collection represented by the passed enumerator object. Reverse enumeration process will be achieved by accessing each element in the collection by using its index. The method will throw an exception if the collection doesn't have a Count
and an Item
properties, so the collection should be derived from IList or IList<> interfaces.
public static IEnumerable ReverseEnum(IEnumerable _enumerator)
{
System.Reflection.PropertyInfo countprop =
_enumerator.GetType().GetProperty("Count", typeof(int));
if (countprop == null)
throw new ArgumentException
("Collection doesn't have a Count property, cannot enumerate.");
int count = (int)countprop.GetValue(_enumerator, null);
if (count<1)
throw new ArgumentException("Collection is empty");
System.Reflection.PropertyInfo indexer =
_enumerator.GetType().GetProperty("Item", new Type[] { typeof(int) });
if (indexer == null)
throw new ArgumentException
("Collection doesn't have a proper indexed property, cannot enumerate.");
for (int i = count - 1; i >= 0; i--)
yield return indexer.GetValue(_enumerator, new object[] { i });
}
For this method, the sample code will traverse the Values collection only, not the key/value pairs:
Console.WriteLine("Reverse enumeration:");
foreach (string value in Enumerators.ReverseEnum(list.Values))
{
Console.WriteLine(" " + value.ToString());
}
Console.WriteLine(" (finished)\r\n");
OUTPUT
Reverse enumeration:
ten
nine
eight
seven
six
five
four
three
two
one
(finished)
Combining Enumerators
The enumerators can be combined (chained) in any way, but you should be aware about the effect produced. It is not the same as circular-stepped enumerator or a stepped-circular enumerator. The sample code combines all the four kinds of enumerators in a single foreach
statement:
Console.WriteLine
("Combined Constrained(2,6)-Stepped(3)-Circular-Reverse enumeration:");
i = 0;
foreach (int value in Enumerators.ConstrainedEnum
(Enumerators.SteppedEnum(Enumerators.CircularEnum
(Enumerators.ReverseEnum(list.Keys)), 3), 2, 6))
{
Console.WriteLine(" " + value.ToString());
if (++i >= max * 2)
break;
}
Console.WriteLine(" (finished)");
OUTPUT
Combined Constrained(2,6)-Stepped(3)-Circular-Reverse enumeration:
4
1
8
5
2
9
(finished)
Using the Source Code
The provided source code is built using Visual C# 2008, so trying to compile the solution in a lower version may not be possible. Anyway, you can simply create a new console project and attach the Program.cs and Enumerators.cs source files. To use the Enumerators
class into your own project, you just need the last one.
History
- August 30, 2008: First version