103. Enumerables#

Enumerables in .NET are a native implementation of the Iterator pattern, allowing you to iterate through collections in a decoupled fashion.

In .NET we have the interface IEnumerable which correspond to the idea of the ‘iterable’ (or ‘aggregator’) from the Iterator pattern and the interface IEnumerator which corresponds to the idea of the ‘iterator’.

Key point

Enumerables offer a way to access elements in a collection sequentially, without exposing the underlying details, just like the Iterator pattern.

../_images/cover-enumerables.jpg

Fig. 103.1 Imagine walking through a cave like maze with a flashlight, seeing only one step ahead, yet confident you can explore the entire maze. This is what enumerables are like in programming. They allow you to traverse through collections, one element at a time, without needing to understand its underlying structure.#

What is an Enumerable?#

An enumerable is an object that implements the IEnumerable interface. This interface has a single method called GetEnumerator, which returns an object implementing the IEnumerator interface. This IEnumerator object then allows for the sequential traversal of elements in the collection.

Since the foreach loop has native support for IEnumerable we don’t have to explicitly extract and traverse the enumerator when we pass an enumerable to a foreach loop. Here’s a simple example using a list:

// Create a List and upcast to IEnumerable.
IEnumerable<int> numbers = new List<int>() { 1, 2, 3 };

// Using foreach loop with IEnumerable.
foreach (int number in numbers)
    Console.WriteLine(number);
1
2
3

Note

Remember, List<T> can be upcast to IEnumerable<T> because it implements the interface ICollection<T> which implements the interface IEnumerable<T>.

Relation to Iterator pattern#

If we look at what’s happening under the hood we can start to see how this is a native implementation of the Iterator pattern. Let’s extract the enumerator and then traverse it manually.

// Create a List and upcast to IEnumerable.
IEnumerable<int> numbers = new List<int>() { 10, 20, 30 };

// Get IEnumerator from IEnumerable.
IEnumerator<int> enumerator = numbers.GetEnumerator();

// Iterate using the enumerator (not enumerable!).
while (enumerator.MoveNext())
{
    // Grab the current item.
    int current = enumerator.Current;

    Console.WriteLine(current);
}
10
20
30

The code above essentially looks the same as the code that we saw in the chapter on the Iterator pattern.

First value of an enumerator#

What is the first value in the sequence of a new enumerator? In the chapter on Iterator pattern we set the first value to the first value of the sequence. This meant that we had to use a do while loop rather than a while loop since we would otherwise skip the first element. Have a look back in the chapter on Iterator pattern if this assertion doesn’t make any sense.

Warning

Before you’ve called MoveNext on the enumerator for the first time, the Current value is the default value of the data type of the enumerator.

The behavior of enumerators in .NET is however defined slightly differently. To enable programmers to use while rather than having to use do while loops the .NET language designers have opted to let the initial value of an enumerator be a value that’s not part of the sequence. The initial value of an enumerator is instead the default value of the data type aggregated in the enumerator.

Let’s take List<int> for example.

// Create a list of integers.
List<int> numbers = new List<int>() { 10, 20, 30 };

// Get an enumerator from the list.
IEnumerator<int> enumerator = numbers.GetEnumerator();

// Without first calling MoveNext, ask for the Current element.
Console.WriteLine(enumerator.Current);
0

In the code above we do not end up with the value 10, which is the first value in the enumerable. Instead we get the value 0, which is the default value of the data type int.

You can think of this as that the first value that the enumerator is pointing to is a value not part of the sequence. When we’re using the enumerator in a foreach loop, we will never see this inital value because MoveNext is called immediately.

Warning

Under ‘normal use’, the initial value of an enumerator should not be exposed. Nevertheless, you should be aware of this risk so that you do not diverge from ‘normal use’.

Question

Compare this behavior to the behavior that we defined in the chapter on Iterator pattern. What are the benefits and drawbacks of this design?

Conclusion#

Enumerables are an embodiment of the Iterator pattern. They offer a decoupled, simple, and flexible way to traverse collections.

In the next chapter we will discuss the yield statement, which greatly simplifies the process of writing custom iterators. In a later chapter, we will discuss LINQ. The way that enumerables intergrate with LINQ makes both a must-know concept for any C# developer worth their salt. Do not sleep on enumerables.