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:
Iterator
is an interface that defines a set of methods for iterating elements in a collection. Typically, theIterator
interface consists of theCurrent
property, theMoveNext()
method, andReset()
method. TheCurrent
property 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.ConcreteIterator
is a class that provides a concrete implementation of theIterator
interface that iterates elements in the collection.Aggregate
is an interface that has thecreateIterator()
method for creating anIterator
object. Typically, you define a collection class that implements theAggregate
interface, and use theIterator
object to iterate the elements in the collection object.ConcreteAggregate
is a concrete implementation of theAggregate
interface that creates aConcreteIterator
object 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
_months
private field holds an array of months fromJan
toDec
. - The
FiscalMonthStart
property defaults to 1, indicating that the fiscal month starts in January by default. - The
CreateIterator()
method returns a newMonthIterator
which is the concrete iterator of theIMonthIterator
interface. The constructor of theMonthIterator
accepts two arguments, the_months
array, 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
_index
field holds the current position of the month in the_months
array. - The
_months
array stores an array of months from Jan to Dec. - The
_count
field 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_months
to return. - The
_fiscalMonthStart
stores the month number that the fiscal month will start e.g._fiscalMonthStart
is 1, which means the fiscal month will start in January. - The constructor validates the
_months
array to make sure that it has 12 elements and also ensures that the value offiscalMonthStart
is from 1 to 12. It assigns_months
and _fiscalMonthStart
fields and initializes the_index
field. - The
Current
getter throws anIndexOutOfRangeException
error if the_index
is not in the valid range from 1 to 12, increases the_count
, and returns the current element in the_months
array based on the_index
. - The
MoveNext()
method returns false if the_count
is greater than 12. Otherwise, it increases the_index
by one and returns true. If the_index
is the same as the length of the_month
array, reset it to zero. - The
Reset()
method resets the_count
to zero and_index
to_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 Mar
Code 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 Mar
Code 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
foreach
to iterate elements in a collection that implement theIEnumerable<T>
interface.