Enumerations
In addition to collections, C# also has the concept of enumerations (enumerables) which encapsulate the concept of an iterable data structure, meaning an enumeration can be iterated through. All collections implement the IEnumerable<T> interface and are enumerations. Enumerations expose an enumerator, which in C# is equivalent to an iterator for traversing the collection. An enumerator/iterator is an object that is associated with an instance of a collection and keeps track of its current position in that collection in order to implement traversal directives like foreach.
Since all collections implement the IEnumerable<T> interface (for dictionaries/maps, it's IEnumerable<KeyValuePair<TKey, TValue>>), they come with data stream operations in the form of Language INtegrated Queries (LINQ).
By obtaining the iterator for a collection through the IEnumerable<T> interface, functions for various generic operations on the resulting data stream can be created. These functions are LINQ methods and are extension methods for IEnumerable<T> (we'll learn about extension methods in the future).
However, before that, let's explore how we can obtain an IEnumerable without an object. We can use the yield keyword in a function that returns an IEnumerable parameterized by a type, when we use yield return with a value of that type, as shown in the example below.
public static IEnumerable<int> Generate(int start, int end)
{
for (var i = start; i < end; ++i)
{
Console.WriteLine("Returning from generator {0}", i);
yield return i;
}
}
La foreach, the code is equivalent to the following:
// If we leave ToList() here, the function will execute completely; otherwise, execution will be lazy.
var list = Generate(1, 23).ToList();
// The following code is equivalent to a foreach loop.
using var enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
var item = enumerator.Current;
Console.Write("{0} ", item);
}
When the function is called with yield, an IEnumerable will be returned. When MoveNext is called on the enumerator to extract a value, the function will execute until a value is returned. At that point, the execution of the function is paused until the next iteration. It resumes from the point where it left off and continues executing until the next yield return or until the function with yield is completed.
It's important to understand that iterating over an IEnumerable is lazy, meaning that values are calculated on-the-fly during iteration, not before it starts. This has the advantage that unnecessary computations won't be processed. However, the disadvantage is that an IEnumerable returned by a function with yield can be used only once. Alternatively, you can use methods like ToList, ToHashSet, or ToArray to materialize that IEnumerable into a collection.
When using an enumerator generated from a collection, avoid modifying the collection while iterating through it. Errors may occur, and the program may crash if such modification is attempted. This holds true for any type of iterator in any programming language that supports iterators.