If you’ve ever used a foreach
loop in C#, you’ve already been using IEnumerable
.
It’s one of the most fundamental interfaces in .NET and forms the foundation of LINQ.
What is IEnumerable?
IEnumerable<T>
represents a sequence of elements that you can iterate over, one by one.
It powers the foreach
loop.
var numbers = new List<int> { 1, 2, 3 };
foreach (var n in numbers)
{
Console.WriteLine(n);
}
Behind the scenes, foreach
calls GetEnumerator()
, which returns an iterator.
The Interface Definition
Here’s the simplified definition of IEnumerable<T>
:
public interface IEnumerable<out T>
{
IEnumerator<T> GetEnumerator();
}
It’s surprisingly simple. It just promises that any type implementing it can return an IEnumerator<T>
.
And what’s IEnumerator<T>
? It’s another interface that knows how to move through a sequence:
public interface IEnumerator<T> : IDisposable
{
T Current { get; }
bool MoveNext();
void Reset(); // rarely used in practice
}
Iterating Without foreach
Since foreach
is just syntactic shortcut, you can do the same manually:
var numbers = new List<int> { 1, 2, 3 };
var enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext())
{
var current = enumerator.Current;
Console.WriteLine(current);
}
Custom Implementation Example
You can also implement IEnumerable<T>
yourself.
Here’s a simple example that generates even numbers up to a maximum value:
public class EvenNumbers(int last) : IEnumerable<int>
{
public class EvenNumber(int last) : IEnumerator<int>
{
private int _current = -2;
public bool MoveNext()
{
_current += 2;
return _current <= last;
}
public void Reset() => _current = -2;
int IEnumerator<int>.Current => _current;
object? System.Collections.IEnumerator.Current => _current;
public void Dispose() { }
}
public IEnumerator<int> GetEnumerator() => new EvenNumber(last);
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => this.GetEnumerator();
}
Usage:
var evens = new EvenNumbers(10);
foreach (var e in evens)
Console.WriteLine(e);
// Output: 0, 2, 4, 6, 8, 10
Notice that the _current
always starts in an invalid position and has the right value after the first call of MoveNext()
.
Why is it Important?
LINQ methods like .Where()
, .Select()
, and .OrderBy()
all extend IEnumerable<T>
.
Without it, LINQ wouldn’t exist.
Example:
var numbers = Enumerable.Range(1, 5);
var squared = numbers.Select(x => x * x);
Console.WriteLine(string.Join(", ", squared));
// Output: 1, 4, 9, 16, 25
Deferred Execution
A powerful feature of IEnumerable<T>
is deferred execution.
LINQ queries aren’t run immediately, they’re only executed when you iterate.
var numbers = Enumerable.Range(1, 5);
var query = numbers.Where(n => n % 2 == 0);
// Nothing happens yet!
foreach (var n in query)
Console.WriteLine(n); // Execution happens here
This allows efficient chaining of operations without unnecessary work.
Performance Notes
-
IEnumerable<T>
only supports forward iteration. You can’t access items by index or know the count without iterating. - For random access or collection size, use
ICollection<T>
orIList<T>
. - Best suited for streaming data and large sequences where you don’t want everything in memory at once.
Key Takeaways
-
IEnumerable<T>
defines the contract for iteration in C#. - It powers
foreach
and enables LINQ. -
IEnumerator<T>
drives iteration withMoveNext()
andCurrent
. - LINQ queries on
IEnumerable<T>
use deferred execution. - For advanced operations like indexing, prefer
IList<T>
orICollection<T>
.
✅ That’s all, folks!
💬 Let’s Connect
Have any questions, suggestions for improvement, or just want to share your thoughts?
Feel free to leave a comment here, or get in touch with me directly on LinkedIn — I’d love to connect!
Top comments (0)