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

In other words, it is basically an element that,

  • Allows iterating over a series of elements
  • Contains a reference to the current element Current and the next one 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 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 GenerateEvens 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’s likely to end up 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 executed in a deferred manner. That is, they do not execute until the collection is iterated over.

This is a very interesting feature, but it can also be a source of errors if not understood 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 this 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 therefore its sum.