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 oneMoveNext()
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.