Click here to Skip to main content
15,867,686 members
Articles / Mobile Apps

Custom Enumerators

Rate me:
Please Sign up or sign in to vote.
4.25/5 (7 votes)
30 Aug 2008CPOL3 min read 67K   216   30   14
Modify the behaviour of any enumerator to make it circular, constrained or stepped. Also reversible enumerator.

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.

C#
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.

C#
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:

C#
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:

C#
const int max = 15;  // Max number of iterations to stop circular enumerator

Having defined a stop behaviour, the circular enumerator can be used as:

C#
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;   // stop circular enumerator, will be infinite if not
}
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.

C#
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:

C#
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.

C#
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:

C#
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.

C#
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:

C#
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:

C#
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;   // stop circular enumerator, will be infinite if not
}
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
Peru Peru


Computer Electronics professional, Software Architect and senior Windows C++ and C# developer with experience in many other programming languages, platforms and application areas including communications, simulation systems, PACS/DICOM (radiology), GIS, 3D graphics and HTML5-based web applications.
Currently intensively working with Visual Studio and TFS.

Comments and Discussions

 
QuestionReverse and Contrained useless? Pin
Mauro Sampietro8-Jun-15 4:43
Mauro Sampietro8-Jun-15 4:43 
AnswerRe: Reverse and Contrained useless? Pin
Jaime Olivares13-Jul-15 13:49
Jaime Olivares13-Jul-15 13:49 
GeneralFew questions Pin
Vikas Misra(TCS)2-Sep-08 20:25
Vikas Misra(TCS)2-Sep-08 20:25 
GeneralRe: Few questions Pin
Jaime Olivares5-Sep-08 4:50
Jaime Olivares5-Sep-08 4:50 
GeneralFunction Extensions Pin
eyanson31-Aug-08 4:55
eyanson31-Aug-08 4:55 
GeneralRe: Function Extensions Pin
Jaime Olivares1-Sep-08 11:12
Jaime Olivares1-Sep-08 11:12 
GeneralRe: Function Extensions Pin
Qwertie9-Sep-08 14:36
Qwertie9-Sep-08 14:36 
GeneralRe: Function Extensions Pin
Jaime Olivares9-Sep-08 14:40
Jaime Olivares9-Sep-08 14:40 
GeneralEnumerators must be disposed Pin
Daniel Grunwald30-Aug-08 22:02
Daniel Grunwald30-Aug-08 22:02 
An enumeration may have unmanaged resources - e.g. take this method:
IEnumerable<string> ReadLines(string filename) {
 using (StreamReader r = new StreamReader(filename)) {
   string line;
   while ((line = r.ReadLine()) != null)
     yield return line;
 } 
}


The StreamReader will only be disposed when the enumerator is disposed. Foreach-loops automatically dispose the enumerator, but if you manually call .GetEnumerator(), you have to do it yourself to prevent leaks!
GeneralRe: Enumerators must be disposed Pin
Jaime Olivares1-Sep-08 11:17
Jaime Olivares1-Sep-08 11:17 
GeneralRe: Enumerators must be disposed Pin
Daniel Grunwald1-Sep-08 12:36
Daniel Grunwald1-Sep-08 12:36 
GeneralUnachievable goal Pin
PIEBALDconsult30-Aug-08 16:19
mvePIEBALDconsult30-Aug-08 16:19 
GeneralRe: Unachievable goal Pin
Jaime Olivares30-Aug-08 16:24
Jaime Olivares30-Aug-08 16:24 
GeneralRe: Unachievable goal Pin
Jaime Olivares30-Aug-08 16:26
Jaime Olivares30-Aug-08 16:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.