| Type | Behavioral |
| Purpose | Provide a way to access the elements in an aggregate object sequentially without exposing the underlying representation. |
Description
The iterator pattern is an abstraction over a collection. It provides a way to access the elements of an aggregate object (like a collection) sequentially without exposing its underlying representation.
Overview
| Aggregate | The aggregate defines an interface for creating an iterator, for example IEnumerable<string> |
| ConcreteAggregate | The concrete object that implements the Aggregate interface and provides an iterator over its elements. |
| Iterator | The iterator defines an interface for accessing and traversing elements, for example the IEnumerator<string> interface. |
| ConcreteIterator | This is the actual implementation of the iterator. |
| Client | The entity that uses the Iterator to access and traverse the elements of the collection or aggregate object, without needing to know about the underlying structure of the collection. |
Implementation
IEnumerable and IEnumerator are central to iteration in .NET. In early C#, custom iterators often required manually implementing IEnumerator, whereas today iterator implementations can frequently be expressed more simply using iterator methods with yield. LINQ also provides a rich set of operations over IEnumerable<T> sequences.
public readonly record struct
Customer(string Name, string Contact, bool IsPreferred = false);
public static class IteratorDemo
{
private static readonly List<Customer> Customers =
[
new("John", "0123456789", IsPreferred: true),
new("Jane", "9876543210"),
new("Jack", "1234567890"),
new("Jill", "6789012345", IsPreferred: true),
];
public static void Execute()
{
Console.WriteLine("Preferred Customers:");
foreach (var name in PreferredCustomerNames)
{
Console.WriteLine(name);
}
}
private static IEnumerable<string> PreferredCustomerNames
=> Customers.Where(c => c.IsPreferred).Select(c => c.Name);
}
Notes
Referring to the code above:
- Aggregate: IEnumerable<string> represents the abstraction through which the sequence can be enumerated.
- ConcreteAggregate: The LINQ expression returned by PreferredCustomerNames produces an IEnumerable<string> sequence backed by the underlying Customers collection and LINQ operators.
- Iterator: IEnumerator<string> represents the interface used to traverse the sequence.
- ConcreteIterator: The concrete enumerator is supplied by the implementation of the LINQ-generated enumerable when GetEnumerator() is called. Client code normally does not need to know its concrete type.
In C#, IEnumerable is pervasive:
private static IEnumerable<string> PreferredCustomerNames()
{
foreach (var customer in Customers)
{
if (customer.IsPreferred)
yield return customer.Name;
}
}
These interfaces and their implementations abstract away much of the implementation detail of the Iterator pattern.
In C# 1.0, enumerators were a bit more work:
public class MyConcreteIterator : IEnumerator
{
private object[] items;
private int position = -1;
public MyConcreteIterator(object[] items)
{
this.items = items;
}
public object Current => items[position];
public bool MoveNext()
{
if (position < items.Length - 1)
{
position++;
return true;
}
return false;
}
public void Reset()
{
position = -1;
}
}
The ConcreteIterator, in the context of the Iterator pattern, is the one responsible for the actual traversal of the collection. The key tasks of a ConcreteIterator include:
- Keeping track of the current position within the aggregate (collection)
- Determining the sequence in which elements are traversed
- Providing a way to access the current element
- Checking if there are more elements to traverse
- Potentially handling the functionality to reset the iterator back to its initial position
Summary
For more information, please see here.