Language: EN

csharp-enumerables

What are Enumerables in C#

In C#, the term “enumerable” refers to any object that implements the IEnumerable or IEnumerable<T> interface. These interfaces provide a mechanism to iterate over a collection of elements.

IEnumerable and IEnumerator Interfaces

The IEnumerable interface defines a single method GetEnumerator(), which returns an IEnumerator. The enumerator provides the necessary functionality 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> in System.Collections.Generic.

IEnumerator Interface

On the other hand, 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 MoveNext()

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);
}

Implementing a Custom Enumerable

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

Here is 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 perform queries on 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 behavior. It will likely 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 in a deferred manner. That is, they are not executed until you iterate over the collection.

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,
  • We use Where to filter numbers greater than 3
  • We display the sum, which is 9 (4 + 5)
  • We add a 6
  • We display the sum, which is now 16

This happens because Where is a method that works with enumerables. Therefore, filtered is not a collection of numbers, but an iterable that depends on numbers.

By adding the number 6 to numbers, filtered is modified, and thus its sum.