IEnumerable makes no sense until two more basic ideas click: what an interface really is, and the difference between the type a variable is declared as and the object it actually holds. This first part builds both from zero, then turns them into the cursor idea that foreach and LINQ run on.
This is the first part of a from-scratch series on IEnumerable<T>. It deliberately spends most of its time before IEnumerable — on interfaces and objects — because that is where the real confusion lives. Once those click, IEnumerable stops being mysterious and becomes an ordinary example of ideas already understood. Every step has code to run.
class Dog
{
public string Name;
}
Dog d = new Dog(); // new Dog() builds a real object; d points at it
d.Name = "Rex";Dog is the blueprint. d is a real object living in memory. Objects hold data and can have behaviour (methods). Nothing surprising yet — this step just fixes the words class and object, because the rest depends on telling them apart.
An interface is a list of method names with no bodies. It promises that any type implementing it will have those methods — but it does nothing itself.
interface IAnimal
{
string Speak(); // no body, no code — just the promise
}Think of it as a job description: "must be able to make coffee." The job description does not make coffee. It only says whoever takes the job must be able to.
This is the exact thing that confuses people about
IEnumerable. The worry "but the interface has no implementation" is not a problem to solve — it is what every interface is. An interface never has code inside it. The code always lives somewhere else, which is the next step.
The real work is written in classes that promise to fulfil the interface:
class Dog : IAnimal
{
public string Name = "Rex"; // Dog's own member — not part of IAnimal
public string Speak() => "Woof"; // Dog's real implementation
}
class Cat : IAnimal
{
public string Speak() => "Meow"; // Cat's real implementation
}Dog : IAnimal reads as "Dog promises to be an IAnimal, and here is how." The body of Speak() lives in Dog and in Cat — never in IAnimal.
This is the most important step in the whole article. A variable can be declared as the interface, while the object it points at is always a concrete class:
IAnimal a = new Dog(); // type is IAnimal, object is a Dog
Console.WriteLine(a.Speak()); // "Woof" — the Dog's version runs
a = new Cat(); // same variable, now points at a Cat
Console.WriteLine(a.Speak()); // "Meow" — the Cat's version runsTwo different things are in play, and keeping them apart is the key to everything later:
| Declared type | Actual object | |
|---|---|---|
| What it is | the promise | the worker |
| Example | IAnimal a | new Dog() |
| Has the real code? | no | yes |
Can it be created with new? | no | yes |
Calling a.Speak() runs the actual object's version. The interface never needed a body, because there is always a concrete object behind the variable supplying the real code. And there is always one — new IAnimal() is illegal; an interface cannot be created on its own. There is always a Dog or a Cat there.
Sit with this one. "A variable typed as an interface always points at a real object that has the code" is the entire answer to "how can
IEnumerablebe iterated if it has no implementation." The rest of the article is this same idea wearing collection names.
The declared type does more than pick which version of a shared method runs — it also decides what is visible at all. Through an IAnimal reference, only the things IAnimal promises can be touched. Anything extra the concrete class happens to have is invisible, even though it is really there:
IAnimal a = new Dog();
Console.WriteLine(a.Speak()); // works — Speak is promised by IAnimal
Console.WriteLine(a.Name); // COMPILE ERROR — IAnimal makes no promise about NameThe object genuinely is a Dog, and Name genuinely exists in memory — but the reference is typed IAnimal, so the compiler allows only what IAnimal guarantees. To reach Name, the variable has to be typed as Dog, or cast back to it:
Dog d = new Dog();
Console.WriteLine(d.Name); // works — d is typed as Dog
IAnimal a = new Dog();
Console.WriteLine(((Dog)a).Name); // works — cast back to Dog firstThis is exactly how
IEnumerablebehaves. When a method returnsIEnumerable<T>, the caller can only doIEnumerable<T>things with it — walk it, run LINQ on it. Even if the real object behind it is aList<T>withAdd,Count, and an indexer, none of those are reachable through theIEnumerable<T>reference. The narrow type is a deliberate limit that says "treat this as a sequence, nothing more" — not a bug to work around.
Now the actual problem. There are many kinds of "bag of items" — a List, an array, a HashSet — and each stores its contents completely differently inside. The goal is one way to go through any of them, so that foreach and LINQ can work on all of them without caring which is which.
The trick every collection agrees to: hand out a cursor — a small thing that points at one item and can step forward. Like the numbered ticket at a deli counter: it marks the current position, and it can advance to the next.
The cursor is IEnumerator<T>. Its promise is "I can step forward and tell the current item":
interface IEnumerator<T>
{
bool MoveNext(); // step to the next item; false when none are left
T Current { get; } // the item the cursor is sitting on right now
}The collection is IEnumerable<T>. Its one promise is "I can give you a cursor":
interface IEnumerable<T>
{
IEnumerator<T> GetEnumerator(); // here is a fresh cursor over my items
}Line these up against Step 4 and they are the same shape:
| Interface | Its promise | Key members |
|---|---|---|
IEnumerable<T> | "I can give you a cursor" | GetEnumerator() |
IEnumerator<T> | "I am the cursor" | MoveNext(), Current |
IEnumerable<T> is just like IAnimal, and GetEnumerator() is just like Speak() — a promise with no body. The bodies live in the real collections.
foreach is not magic. This loop:
foreach (var item in things)
Console.WriteLine(item);is exactly this underneath:
var cursor = things.GetEnumerator(); // ask for a cursor
while (cursor.MoveNext()) // step forward until none are left
Console.WriteLine(cursor.Current); // read the item at the cursorSo foreach needs only one thing from things: a working GetEnumerator(). That is the whole reason anything implementing IEnumerable<T> can be used in a foreach.
Exactly as Dog and Cat wrote the real Speak(), each concrete collection writes the real GetEnumerator(). List<T> implements it to walk its internal array; HashSet<T> implements it to walk its hash buckets; and so on.
IEnumerable<int> e = new List<int> { 1, 2, 3 }; // type: IEnumerable, object: a List
foreach (var x in e)
Console.WriteLine(x); // runs List's GetEnumerator — the REAL implementationThe interface is empty; the List object behind it is not. This is the Dog behind the IAnimal, one more time.
Now the question that started this series answers itself. When Where "returns an IEnumerable<T>," it does not return the empty interface. It returns a real object whose declared type happens to be IEnumerable<T>:
var numbers = new[] { 1, 2, 3, 4, 5 };
IEnumerable<int> q = numbers.Where(x => x > 2);
Console.WriteLine(q.GetType().Name); // NOT "IEnumerable" — a real class, e.g. "WhereArrayIterator`1"That real object is a small class (the compiler generates it from the yield return inside Where) that has a genuine GetEnumerator, MoveNext, and Current. Running foreach over q runs that object's implementation — never the interface's, because interfaces have none.
Same rule as the Dog behind the IAnimal: the type is the promise, the object does the work. There is no such thing as iterating "the interface" — always some concrete object's cursor is doing the walking.
The best way to make this concrete is to write a type that implements IEnumerable<T> and watch foreach accept it. This tiny class counts down, and it is fully runnable:
using System.Collections;
using System.Collections.Generic;
class Countdown : IEnumerable<int>
{
private readonly int _from;
public Countdown(int from) => _from = from;
// the real implementation of the promise:
public IEnumerator<int> GetEnumerator()
{
for (int i = _from; i >= 1; i--)
yield return i; // hand out each number, one at a time
}
// the older non-generic version, required because IEnumerable<T> extends IEnumerable:
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
// use it exactly like any built-in collection:
foreach (var n in new Countdown(3))
Console.WriteLine(n); // 3, 2, 1Countdown is now a real object that keeps the IEnumerable<int> promise, so foreach works on it — because foreach only ever needed a GetEnumerator(). The yield return writes the cursor for us; the second GetEnumerator is boilerplate required because IEnumerable<T> is built on the older non-generic IEnumerable. Both details get their own part later; for now the point is that a hand-written type slots straight into foreach.
An interface is a promise with no code. A variable typed as an interface always points at a real object that does have the code. So "iterating an
IEnumerable" always means running some concrete object'sGetEnumerator— whether that object is aListwritten by the framework, aCountdownwritten by hand, or an iterator written by the compiler for a LINQ query.
Part 2 goes one level deeper into the cursor itself — IEnumerator<T>, the MoveNext/Current dance, what "the cursor starts before the first item" means, and how foreach cleans the cursor up afterward. From there the series moves on to yield return in full, deferred execution and the one trap it sets, and finally the wider family of collection interfaces. Everything builds on the single idea from Step 4: the type is the promise, the object does the work.