Language: EN

csharp-enumerables

What are Enumerables in C#

In C#, enumerables are a mechanism to iterate over collections of elements. For example, they can be used in FOR EACH loops or combined with LINQ.

Technically, an enumerable refers to any object that implements the IEnumerable or IEnumerable<T> interface.

Let’s see it in detail 👇.

IEnumerable and IEnumerator Interface

The IEnumerable interface defines a single method GetEnumerator(), which returns an IEnumerator. The enumerator provides the functionality needed to iterate through a collection.

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

There is also the generic version IEnumerable<T>, which inherits from IEnumerable.

public interface IEnumerable<out T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

The IEnumerable interface is defined in the System.Collections namespace, and its generic version IEnumerable<T> is in System.Collections.Generic.

IEnumerator Interface

On its part, the IEnumerator interface and its generic equivalent IEnumerator<T> look like this.

public interface IEnumerator
{
    bool MoveNext();
    object Current { get; }
    void Reset();
}

public interface IEnumerator<out T> : IEnumerator
{
	T Current { get; }
}

That is, it is basically an element that,

  • Allows iterating over a series of elements
  • Contains a reference to the current element Current and the next ModeNext()

How to Use Enumerables

The most common way to iterate over an enumerable is by using foreach. This loop simplifies the iteration syntax by hiding the details of the enumerator.

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

foreach (int number in numbers)
{
    Console.WriteLine(number);
}

Implementation of a Custom Enumerable

We can create a class that implements IEnumerable<T> to define a custom collection.

Here we have an example of an Enumerable that generates a sequence of even numbers.

public class Evens : IEnumerable<int>
{
    private int _max;

    public Evens(int max)
    {
        _max = max;
    }

    public IEnumerator<int> GetEnumerator()
    {
        for (int i = 0; i <= _max; i += 2)
        {
            yield return i;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

Using yield return

The yield return keyword simplifies the creation of custom enumerators. We can use it to return elements on the fly.

public static IEnumerable<int> GenerateEvens(int max)
{
    for (int i = 0; i <= max; i += 2)
    {
        yield return i;
    }
}

In this example, the method GetEvenNumbers uses yield return to return even numbers up to a specified maximum.

Using LINQ with IEnumerable

LINQ (Language Integrated Query) allows us to query collections in a declarative manner. LINQ queries work with any collection that implements IEnumerable<T>.

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var evenNumbers = from number in numbers
                  where number % 2 == 0
                  select number;

foreach (int number in evenNumbers)
{
    Console.WriteLine(number);
}

Avoid Modifying Collections During Iteration

Modifying a collection while iterating over it can cause exceptions or unexpected behaviors. It is likely to end in 💥.

Instead, we should create a new collection with the modified elements.

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
List<int> duplicatedNumbers = new List<int>();

foreach (int number in numbers)
{
    duplicatedNumbers.Add(number * 2);
}

Understanding Deferred Execution

LINQ queries and methods that use yield return are evaluated deferred. That is, they are not executed until the collection is iterated over.

This is a very interesting feature, but it can also be a source of errors if we do not understand it correctly.

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var filtered = numbers.Where(n => n > 3);
Console.WriteLine(filtered.Sum());  // 9

numbers.Add(6);

Console.WriteLine(filtered.Sum());  // 15

In the example,

  • We create a collection of numbers from 1 to 5,
  • Use Where to filter numbers greater than 3
  • Show the sum, which is 9 (4 + 5)
  • Add a 6
  • Show the sum, which is now 16

This is because Where is a method that works with enumerables. Therefore, filtered is not a collection of numbers, but an iterable that depends on numbers. When we add the number 6 to numbers, filtered is modified, and hence its sum.