1. Introduction
As you might've already noticed, different C# collections have some family traits. For example, you can loop through almost any of them using a foreach loop:
var names = new List<string> { "Anya", "Boris", "Vika" };
// Heads up — this code works with List, arrays, and even HashSet!
foreach (var name in names)
{
Console.WriteLine(name);
}
What's the magic? It's all because all modern collections implement the IEnumerable<T> interface — kind of a "contract" that guarantees the collection can give you its elements one by one, in order.
Imagine a company where for any collection of employees (could be a department, a project team, or a party guest list) there's a rule: if an object implements "IEnumerable", you can always go through all the employees in a certain order, no matter "how they're stored", the main thing is — you can enumerate them.
2. The IEnumerable<T> Interface — What's Inside?
Let's see what this interface looks like in .NET:
public interface IEnumerable<out T>
{
IEnumerator<T> GetEnumerator();
}
It has just one method — GetEnumerator, which returns an object of type IEnumerator<T>. This object is in charge of the actual "enumeration" process: it knows which element is current, how to move to the next one, and when it's all over.
In short: if a class (or collection) implements IEnumerable<T>, you can loop through it with foreach, get elements one by one — and it doesn't matter how they're implemented inside: could be a list, a hash table, or even on a hard drive.
Mini-Diagram
Collection (for example, List<T>)
|
v
IEnumerable<T>
|
v
IEnumerator<T> (the real "element walker")
3. Practice and Code Universality
A huge plus of this contract is universality. If a function or method takes IEnumerable<T> as input, it means it's compatible with any kind of collection — from arrays and lists to hash sets, queues, and even your own custom collections!
For example, check out this function that sums up elements:
// You can sum up any set of int that supports IEnumerable<int>
int Sum(IEnumerable<int> numbers)
{
int result = 0;
foreach (var n in numbers)
result += n;
return result;
}
// Works with List<int>
var list = new List<int> { 1, 2, 3 };
Console.WriteLine(Sum(list));
// Works with arrays!
int[] array = { 4, 5, 6 };
Console.WriteLine(Sum(array));
// Even works with the result of a method that filters elements
Console.WriteLine(Sum(list.Where(x => x % 2 == 0))); // Using LINQ
Real-Life Example: Universal Methods
Imagine you need to write a utility to find the longest word in any collection of strings. Thanks to IEnumerable<string>, you can do this for any source — array, list, result of filter methods, etc.:
string FindLongest(IEnumerable<string> words)
{
string longest = "";
foreach (var word in words)
if (word.Length > longest.Length)
longest = word;
return longest;
}
You can use it with any suitable set.
4. Why Does foreach Work with IEnumerable<T>?
A common question: why does the foreach loop "understand" any collection? The answer is simple: the C# compiler looks for a GetEnumerator method in the class and expects to get an object with MoveNext() and a Current property. That's the standard interface — IEnumerator<T>.
Thanks to this approach, you can even write your own class that "gives out" elements one by one (like a Fibonacci sequence generator), and if it implements IEnumerable<int>, you can use it in foreach just like a regular list.
5. What is an Enumerator and How Does It Work?
Inside any collection that implements IEnumerable<T>, there's a special "walker" — the Enumerator (or, to be nerdy: an object that implements IEnumerator<T>). It's the one that "drags" elements from the collection one by one when you write a foreach loop.
The IEnumerator<T> Interface
Here's what a standard Enumerator can do:
public interface IEnumerator<T> : IDisposable
{
T Current { get; } // Current element
bool MoveNext(); // Move to the next element
void Reset(); // Go back to the start (rarely used)
}
How does it work under the hood?
- MoveNext() — moves the "pointer" to the next element and returns true if there's an element. If there are no more elements — returns false.
- Current — gives you the current element (the one the enumerator is pointing at right now).
- Reset() — resets the Enumerator to the start (but almost never used).
- Dispose() — frees up resources (needed for collections that might work with files or the network).
Example: How foreach Works "Under the Hood"
When you write:
var numbers = new List<int> { 1, 2, 3 };
foreach (var n in numbers)
Console.WriteLine(n);
Actually, the compiler turns it into something like this:
var numbers = new List<int> { 1, 2, 3 };
// Get the "walker"
var enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext())
{
var n = enumerator.Current;
Console.WriteLine(n);
}
// The compiler automatically calls Dispose() in a using block (if enumerator implements IDisposable)
Important: if a collection works with external resources (like files or databases), the Enumerator can free them up automatically when the walk is done.
Visual Diagram
Start of walk -> GetEnumerator() -> Enumerator
|
v
MoveNext() -> Current
|
v
MoveNext() -> Current
|
...
|
v
MoveNext() == false -> end of walk
6. IEnumerable and Arrays, Lists, Sets: Who's Related to Whom
Let's see which standard .NET containers implement this interface:
| Collection Type | Implements IEnumerable<T>? | Can you use foreach? |
|---|---|---|
Array () |
✅ | ✅ |
|
✅ | ✅ |
|
✅ (by pairs, keys, values) | ✅ |
|
✅ | ✅ |
|
✅ | ✅ |
|
✅ | ✅ |
Even a string (string) implements regular IEnumerable, so you can loop through each character in a string the same way.
7. Making Your Own Enumerable
Fun challenge: let's try to make a minimal collection that stores even numbers from 0 to N and implements IEnumerable<int>. Then we can loop through it and use it with LINQ.
// A collection class you can "walk through"
class EvenNumbers : IEnumerable<int>
{
private int max;
public EvenNumbers(int max)
{
this.max = max;
}
public IEnumerator<int> GetEnumerator()
{
for (int i = 0; i <= max; i += 2)
yield return i; // Special magic for implementing the enumerator
}
// Explicit implementation of non-generic IEnumerable for backward compatibility
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
// Usage:
var evens = new EvenNumbers(10);
foreach(var e in evens)
Console.Write($"{e} "); // 0 2 4 6 8 10
Notice the key detail: if your class implements IEnumerable<T> — you automatically make it compatible with most .NET tools: LINQ, foreach, methods that take an enumerable.
8. Typical Mistakes and Gotchas
Sometimes newbie devs think IEnumerable<T> is some kind of separate collection that has elements. Actually — it's just a "promise" that if we start enumerating, we'll be "given" elements one by one.
If you need random access by index (myList[5]), or want to use methods like Add or Remove — the IEnumerable<T> interface won't help. It's only for sequential walking!
Mistake: trying to modify a collection while looping through it. For example:
foreach (var item in myList)
{
if (item < 0)
myList.Remove(item); // DANGEROUS! InvalidOperationException
}
It's better to first make a separate list for removal, or use methods that create a new collection:
// Safe way — create a new collection manually
var newList = new List<int>();
foreach (var item in myList)
{
if (item >= 0)
newList.Add(item);
}
myList = newList;
// Or remove by indexes in reverse order
for (int i = myList.Count - 1; i >= 0; i--)
{
if (myList[i] < 0)
myList.RemoveAt(i);
}
GO TO FULL VERSION