Click here to Skip to main content
Click here to Skip to main content
Go to top

Custom Enumerators

, 30 Aug 2008
Rate this:
Please Sign up or sign in to vote.
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.

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;  // Max number of iterations to stop circular enumerator

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;   // 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.

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;   // 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)

Share

About the Author

Jaime Olivares
Architect Freelance (jaimeolivares.com)
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 C# 2013 and TFS.
Can be reached at http://www.jaimeolivares.com
Follow on   LinkedIn

Comments and Discussions

 
GeneralFew questions PinmemberMember 15911272-Sep-08 20:25 
GeneralRe: Few questions PinmemberJaime Olivares5-Sep-08 4:50 
GeneralFunction Extensions Pinmembereyanson31-Aug-08 4:55 
GeneralRe: Function Extensions PinmemberJaime Olivares1-Sep-08 11:12 
GeneralRe: Function Extensions PinmemberQwertie9-Sep-08 14:36 
GeneralRe: Function Extensions PinmemberJaime Olivares9-Sep-08 14:40 
GeneralEnumerators must be disposed PinmemberDaniel Grunwald30-Aug-08 22:02 
GeneralRe: Enumerators must be disposed PinmemberJaime Olivares1-Sep-08 11:17 
GeneralRe: Enumerators must be disposed PinmemberDaniel Grunwald1-Sep-08 12:36 
GeneralUnachievable goal PinmemberPIEBALDconsult30-Aug-08 16:19 
GeneralRe: Unachievable goal PinmemberJaime Olivares30-Aug-08 16:24 
GeneralRe: Unachievable goal PinmemberJaime Olivares30-Aug-08 16:26 

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

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

| Advertise | Privacy | Mobile
Web04 | 2.8.140921.1 | Last Updated 30 Aug 2008
Article Copyright 2008 by Jaime Olivares
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid