Summary: in this tutorial, you will learn about the C# iterator pattern and how to use it to define an interface that iterates the elements in a collection.
Introduction to the C# Iterator design pattern
The Iterator is a behavioral design pattern that allows you to define an interface for iterating elements of a collection without exposing its underlying implementation. The Iterator pattern does this by separating the iteration logic from the collection object.
The following UML diagram illustrates the C# Iterator pattern:
The Iterator pattern has the following participants:
Iteratoris an interface that defines a set of methods for iterating elements in a collection. Typically, theIteratorinterface consists of theCurrentproperty, theMoveNext()method, andReset()method. TheCurrentproperty returns the current element in the collection. TheMoveNext()method returns true if the next element exists and advances to the next element or returns false if no more elements to iterate. TheReset()is an optional method that resets the iterator to the initial state.ConcreteIteratoris a class that provides a concrete implementation of theIteratorinterface that iterates elements in the collection.Aggregateis an interface that has thecreateIterator()method for creating anIteratorobject. Typically, you define a collection class that implements theAggregateinterface, and use theIteratorobject to iterate the elements in the collection object.ConcreteAggregateis a concrete implementation of theAggregateinterface that creates aConcreteIteratorobject for iterating the collection.
C# Iterator pattern example
Suppose you have a collection of calendar months from January to December, and you want to iterate through it in the sequence of Jan, Feb, Mar, … Dec.
Additionally, if the fiscal month starts in April instead of January, you need to provide a way to iterate the months in the following sequence: Apr, May, … Dec, Jan, Feb, Mar.
To achieve this kind of iteration, you can use the Iterator pattern. Here are the steps:
First, define an Iterator interface that has methods for iterating months:
public interface IMonthIterator
{
string Current { get; }
bool MoveNext();
void Reset();
}Code language: C# (cs)Second, create an IAggregate interface that defines a method for creating an IMonthIterator object:
public interface IAggregate
{
IMonthIterator CreateIterator();
}Code language: C# (cs)Third, define the Months collection that implements the IAggregate interface:
public class Months: IAggregate
{
private readonly string[] _months = {
"Jan","Feb","Mar",
"Apr","May","Jun",
"Jul","Aug","Sep",
"Oct","Nov","Dec",
};
public int FiscalMonthStart { get; set; } = 1;
public IMonthIterator CreateIterator()
{
return new MonthIterator(_months, FiscalMonthStart);
}
}Code language: C# (cs)In the Months class:
- The
_monthsprivate field holds an array of months fromJantoDec. - The
FiscalMonthStartproperty defaults to 1, indicating that the fiscal month starts in January by default. - The
CreateIterator()method returns a newMonthIteratorwhich is the concrete iterator of theIMonthIteratorinterface. The constructor of theMonthIteratoraccepts two arguments, the_monthsarray, and theFiscalMonthStart.
Fourth, define the MonthIterator class that implements the IMonthIterator interface:
public class MonthIterator: IMonthIterator
{
private int _index;
private int _count = 0;
private readonly string[] _months;
private readonly int _fiscalMonthStart;
public MonthIterator(string[] months, int fiscalMonthStart)
{
if (months.Length != 12)
{
throw new ArgumentException("The number of months is not 12");
}
if (fiscalMonthStart < 0 || fiscalMonthStart > 12)
{
throw new ArgumentOutOfRangeException(nameof(fiscalMonthStart));
}
_months = months;
_fiscalMonthStart = fiscalMonthStart;
_index = _fiscalMonthStart - 2;
}
public string Current
{
get
{
if (_index < 0 || _index > _months.Length)
{
throw new IndexOutOfRangeException();
}
_count++;
return _months[_index];
}
}
public bool MoveNext()
{
if (_count >= 12)
{
return false;
}
_index++;
if (_index == _months.Length)
{
_index = 0;
}
return true;
}
public void Reset()
{
_count = 0;
_index = _fiscalMonthStart - 2;
}
}Code language: C# (cs)In the MonthIterator class:
- The
_indexfield holds the current position of the month in the_monthsarray. - The
_monthsarray stores an array of months from Jan to Dec. - The
_countfield holds the number of months that have been iterated. Its values are from 1 to 12. If you iterate all the months, the _count will be 12, and no more next element in the_monthsto return. - The
_fiscalMonthStartstores the month number that the fiscal month will start e.g._fiscalMonthStartis 1, which means the fiscal month will start in January. - The constructor validates the
_monthsarray to make sure that it has 12 elements and also ensures that the value offiscalMonthStartis from 1 to 12. It assigns_monthsand _fiscalMonthStartfields and initializes the_indexfield. - The
Currentgetter throws anIndexOutOfRangeExceptionerror if the_indexis not in the valid range from 1 to 12, increases the_count, and returns the current element in the_monthsarray based on the_index. - The
MoveNext()method returns false if the_countis greater than 12. Otherwise, it increases the_indexby one and returns true. If the_indexis the same as the length of the_montharray, reset it to zero. - The
Reset()method resets the_countto zero and_indexto_fiscalMonthStart - 2.
Finally, create a program that uses the iterator to iterate over months with the fiscal month starting in Jan and April:
public class Program
{
public static void Main(string[] args)
{
var months = new Months();
// fiscal month start in Jan by default
Console.WriteLine("Fiscal month start in January:");
var iterator = months.CreateIterator();
while (iterator.MoveNext())
{
Console.Write($"{iterator.Current} ");
}
Console.WriteLine();
// fiscal month start in April
Console.WriteLine("Fiscal month start in April:");
months.FiscalMonthStart = 4;
iterator = months.CreateIterator();
while (iterator.MoveNext())
{
Console.Write($"{iterator.Current} ");
}
}
}Code language: C# (cs)Output:
Fiscal month start in January:
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
Fiscal month start in April:
Apr May Jun Jul Aug Sep Oct Nov Dec Jan Feb MarCode language: plaintext (plaintext)Put it all together:
namespace IteratorPattern;
public interface IMonthIterator
{
string Current { get; }
bool MoveNext();
void Reset();
}
public interface IAggregate
{
IMonthIterator CreateIterator();
}
public class Months : IAggregate
{
private readonly string[] _months = {
"Jan","Feb","Mar",
"Apr","May","Jun",
"Jul","Aug","Sep",
"Oct","Nov","Dec",
};
public int FiscalMonthStart { get; set; } = 1;
public IMonthIterator CreateIterator()
{
return new MonthIterator(_months, FiscalMonthStart);
}
}
public class MonthIterator : IMonthIterator
{
private int _index;
private int _count = 0;
private readonly string[] _months;
private readonly int _fiscalMonthStart;
public MonthIterator(string[] months, int fiscalMonthStart)
{
if (months.Length != 12)
{
throw new ArgumentException("The number of months is not 12");
}
if (fiscalMonthStart < 0 || fiscalMonthStart > 12)
{
throw new ArgumentOutOfRangeException(nameof(fiscalMonthStart));
}
_months = months;
_fiscalMonthStart = fiscalMonthStart;
_index = _fiscalMonthStart - 2;
}
public string Current
{
get
{
if (_index < 0 || _index > _months.Length)
{
throw new IndexOutOfRangeException();
}
_count++;
return _months[_index];
}
}
public bool MoveNext()
{
if (_count >= 12)
{
return false;
}
_index++;
if (_index == _months.Length)
{
_index = 0;
}
return true;
}
public void Reset()
{
_count = 0;
_index = _fiscalMonthStart - 2;
}
}
public class Program
{
public static void Run(string[] args)
{
var months = new Months();
// fiscal month start in Jan by default
Console.WriteLine("Fiscal month start in January:");
var iterator = months.CreateIterator();
while (iterator.MoveNext())
{
Console.Write($"{iterator.Current} ");
}
Console.WriteLine();
// fiscal month start in April
Console.WriteLine("Fiscal month start in April:");
months.FiscalMonthStart = 4;
iterator = months.CreateIterator();
while (iterator.MoveNext())
{
Console.Write($"{iterator.Current} ");
}
}
}Code language: C# (cs)Implementing Iterator pattern in C# using IEnumerable and IEnumerator interfaces
C# supports the Iterator design pattern out of the box via the IEnumerator and IEnumerable interfaces. For the modern C# code, you should use the IEnumerator<T> and IEnumerable<T> instead.
Also, C# allows you to iterate over the elements of a collection that implements the IEnumerable<T> interface using the foreach loop.
In C#, an iterator is also called an enumerator. The IEnumerable interface has the GetEnumerator() method that returns an enumerator for iterating over elements of a collection. It is equivalent to the Aggregate interface.
The IEnumerator interface has the Current property that returns the current element in the collection, the MoveNext() advances the enumerator to the next element of the collection, and the Reset() method sets the enumerator to its initial position. The IEnumerator is equivalent to the Iterator interface.
The following example shows how to use the IEnumable and IEnumerator interfaces to implement the Iterator pattern.
First, define the Months class that implements the IEnumerable interface. The GetEnumerator() method should return an instance of the IEnumerator interface. In this case, it returns an instance of the MonthIterator class:
public class Months : IEnumerable
{
private readonly string[] _months = {
"Jan","Feb", "Mar",
"Apr","May", "Jun",
"Jul","Aug", "Sep",
"Oct","Nov", "Dec",
};
public int FiscalMonthStart { get; set; } = 1;
public IEnumerator GetEnumerator()
{
return new MonthIterator(_months, FiscalMonthStart);
}
}Code language: C# (cs)Second, define the MonthIterator class that implements the IEnumerator interface:
public class MonthIterator : IEnumerator
{
private int _index;
private int _count = 0;
private readonly int _fiscalMonthStart = 0;
private readonly string[] _months;
public MonthIterator(string[] months, int fiscalMonthStart)
{
if (months.Length != 12)
{
throw new ArgumentException("The number of months is not 12");
}
if (fiscalMonthStart < 1 || fiscalMonthStart > 12)
{
throw new ArgumentOutOfRangeException(nameof(fiscalMonthStart));
}
_months = months;
_fiscalMonthStart = fiscalMonthStart;
_index = _fiscalMonthStart - 2;
}
public object Current
{
get
{
if (_index < 0 || _index > _months.Length)
{
throw new IndexOutOfRangeException();
}
_count++;
return _months[_index];
}
}
public bool MoveNext()
{
if (_count >= 12)
{
return false;
}
_index++;
if (_index == _months.Length)
{
_index = 0;
}
return true;
}
public void Reset()
{
_count = 0;
_index = _fiscalMonthStart - 2;
}
}Code language: C# (cs)Third, use the foreach to iterate the elements of the Months collection:
public class Program
{
public static void Main(string[] args)
{
var months = new Months();
Console.WriteLine("Fiscal month start in January:");
foreach (var month in months)
{
Console.Write($"{month} ");
}
Console.WriteLine();
// fiscal month start in April
Console.WriteLine("Fiscal month start in April:");
months.FiscalMonthStart = 4;
foreach (var month in months)
{
Console.Write($"{month} ");
}
}
}Code language: C# (cs)Output:
Fiscal month start in January:
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
Fiscal month start in April:
Apr May Jun Jul Aug Sep Oct Nov Dec Jan Feb MarCode language: C# (cs)Put it all together:
using System.Collections;
namespace IteratorPattern;
public class Months : IEnumerable
{
private readonly string[] _months = {
"Jan","Feb", "Mar",
"Apr","May", "Jun",
"Jul","Aug", "Sep",
"Oct","Nov", "Dec",
};
public int FiscalMonthStart { get; set; } = 1;
public IEnumerator GetEnumerator()
{
return new MonthIterator(_months, FiscalMonthStart);
}
}
public class MonthIterator : IEnumerator
{
private int _index;
private int _count = 0;
private readonly int _fiscalMonthStart = 0;
private readonly string[] _months;
public MonthIterator(string[] months, int fiscalMonthStart)
{
if (months.Length != 12)
{
throw new ArgumentException("The number of months is not 12");
}
if (fiscalMonthStart < 1 || fiscalMonthStart > 12)
{
throw new ArgumentOutOfRangeException(nameof(fiscalMonthStart));
}
_months = months;
_fiscalMonthStart = fiscalMonthStart;
_index = _fiscalMonthStart - 2;
}
public object Current
{
get
{
if (_index < 0 || _index > _months.Length)
{
throw new IndexOutOfRangeException();
}
_count++;
return _months[_index];
}
}
public bool MoveNext()
{
if (_count >= 12)
{
return false;
}
_index++;
if (_index == _months.Length)
{
_index = 0;
}
return true;
}
public void Reset()
{
_count = 0;
_index = _fiscalMonthStart - 2;
}
}
public class Program
{
public static void Main(string[] args)
{
var months = new Months();
Console.WriteLine("Fiscal month start in January:");
foreach (var month in months)
{
Console.Write($"{month} ");
}
Console.WriteLine();
// fiscal month start in April
Console.WriteLine("Fiscal month start in April:");
months.FiscalMonthStart = 4;
foreach (var month in months)
{
Console.Write($"{month} ");
}
}
}Code language: C# (cs)Summary
- Use the Iterator pattern to define an interface for iterating elements of a collection.
- Use
IEnumerable<T>andIEnumerator<T>interfaces to implement the iterator pattern in C#. - Use
foreachto iterate elements in a collection that implement theIEnumerable<T>interface.