← Open in the full interactive course (progress tracking, search & more)

IEnumerable<T> is the one promise every collection makes: "hand me a way to visit your items, one at a time." foreach is just a polite way of asking for it.

You've now written foreach dozens of times in this module — over arrays, lists, dictionaries, sets, even stacks and queues. It always just works, no matter which collection you point it at. In the previous lesson, you learned why: every one of those types implements IEnumerable<T>. This lesson stops at the surface of that idea and asks: what is IEnumerable<T>, actually, and what does foreach do with it?

This is a gentle first look, not a deep dive — the full mechanics of custom iterators (the yield keyword) and LINQ, which is built entirely on top of IEnumerable<T>, come later in this course. Here, the goal is just a solid, correct mental model of what "enumerable" means and how foreach actually uses it.

What Is It?

The Simple Explanation

Something is enumerable if you can visit each of its items, one after another, in order. IEnumerable<T> is C#'s way of saying "I promise you can do that to me." It doesn't promise a Count. It doesn't promise indexing. It promises exactly one thing: you can walk through my items.

The Technical Definition

IEnumerable<T> is a generic interface with exactly one member:

public interface IEnumerable<T> { IEnumerator<T> GetEnumerator(); }

That's the entire contract. A type is enumerable simply by promising it can produce an enumerator — a separate, small helper object whose entire job is to remember "where am I in this walk-through, and what's the current item?" IEnumerator<T>, the thing GetEnumerator() returns, has its own small contract:

public interface IEnumerator<T> { T Current { get; } // the item at the current position bool MoveNext(); // advance to the next item; false if there isn't one void Reset(); // rarely used — jump back to the start }
Two Different Jobs, Two Different Interfaces

Why Does It Exist?

The Problem

Without a shared contract, every collection type would need its own bespoke way of being looped over — an array would need one kind of loop syntax, a dictionary a different one, a custom collection type you write yourself yet another. Code that wants to "just visit every item" would need a separate code path for every single collection type it might ever encounter, including ones that don't exist yet.

The Solution

IEnumerable<T> gives every collection type — built into .NET, or written by you — one shared, standard way to expose "walk through my items." Once a type implements it, it automatically gets to plug into foreach, and (later in this course) the entire LINQ query system, without either of those needing to know anything specific about how that type stores its data internally.

Big Picture

WHAT foreach REALLY DOES
You write this:
foreach (string name in names)
    Console.WriteLine(name);
▼ the compiler translates it into ▼
IEnumerator<string> enumerator = names.GetEnumerator();
try
{
    while (enumerator.MoveNext())
    {
        string name = enumerator.Current;
        Console.WriteLine(name);
    }
}
finally
{
    enumerator.Dispose();
}
foreach is entirely built on GetEnumerator(), MoveNext(), and Current — nothing more.

How It Works

FROM foreach TO EACH ITEM, STEP BY STEP
1. ASK THE COLLECTION FOR AN ENUMERATOR
2. TRY TO MOVE TO THE NEXT ITEM
3. READ THE CURRENT ITEM
4. REPEAT UNTIL MoveNext RETURNS FALSE

Simple Example

Let's build a tiny custom collection from scratch, and make it work with foreach by implementing IEnumerable<T> ourselves — no shortcuts, so you can see exactly how the pieces fit together.

public class NameList : IEnumerable<string> { private readonly string[] _names; public NameList(params string[] names) => _names = names; // The one member IEnumerable<string> requires: public IEnumerator<string> GetEnumerator() => new NameEnumerator(_names); // Required by the non-generic IEnumerable that IEnumerable<T> extends — // just forwards to the generic version above. System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); // A small helper class that does the actual step-by-step walking. private class NameEnumerator : IEnumerator<string> { private readonly string[] _names; private int _position = -1; // starts BEFORE the first item public NameEnumerator(string[] names) => _names = names; public string Current => _names[_position]; object System.Collections.IEnumerator.Current => Current; public bool MoveNext() { _position++; return _position < _names.Length; } public void Reset() => _position = -1; public void Dispose() { } // nothing to clean up here } } var names = new NameList("Ana", "Ben", "Cara"); foreach (string name in names) Console.WriteLine(name); // Ana // Ben // Cara

Code → Meaning → Result:

Real-World Example

A team builds a small WeeklySchedule class for a shift-planning app — it wraps a fixed 7-day array internally, but the team wants callers to be able to simply foreach over "today's shifts for the week" without knowing or caring that an array is involved underneath.

public record Shift(DayOfWeek Day, string EmployeeName); public class WeeklySchedule : IEnumerable<Shift> { private readonly List<Shift> _shifts = []; public void AddShift(Shift shift) => _shifts.Add(shift); // Because List<T> is already enumerable, we can simply hand back its enumerator — // there's no need to hand-write one ourselves every time. public IEnumerator<Shift> GetEnumerator() => _shifts.GetEnumerator(); System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); } var schedule = new WeeklySchedule(); schedule.AddShift(new Shift(DayOfWeek.Monday, "Amy")); schedule.AddShift(new Shift(DayOfWeek.Tuesday, "Ben")); schedule.AddShift(new Shift(DayOfWeek.Wednesday, "Cara")); // The caller has no idea a List<Shift> is hiding underneath — and doesn't need to. foreach (Shift shift in schedule) Console.WriteLine($"{shift.Day}: {shift.EmployeeName}"); // Monday: Amy // Tuesday: Ben // Wednesday: Cara

Notice this version doesn't need a hand-written enumerator class at all — since _shifts is a List<Shift>, and List<T> is already enumerable, WeeklySchedule can simply delegate to it. This is by far the most common way real code implements IEnumerable<T>: wrap an already-enumerable collection internally, and forward the request.

Analogy

A Playlist and Its Bookmark

Think of a music playlist (the collection) and a physical bookmark you place in it (the enumerator). The playlist itself doesn't "remember" where you currently are — it just holds the songs. The bookmark is a separate little object whose only job is tracking your current position: "move to the next song" (MoveNext) and "tell me which song we're on right now" (Current).

You could hand two different friends two separate bookmarks for the same playlist, and each could be at a completely different song, independently — which is exactly why GetEnumerator() hands back a fresh enumerator each time it's called: every "walk-through" of the collection gets its own independent bookmark, so multiple simultaneous loops over the same collection never interfere with each other.

Under the Hood

A FEW DETAILS WORTH KNOWING AT THIS STAGE
1. foreach IS COMPILER SUGAR, NOT A LANGUAGE-LEVEL SPECIAL CASE
2. WHY THE ENUMERATOR IS A SEPARATE OBJECT FROM THE COLLECTION
3. THIS IS THE FOUNDATION LINQ IS BUILT ON
4. WHAT'S DELIBERATELY OUT OF SCOPE FOR THIS LESSON

Common Confusion

1. IEnumerable<T> vs IEnumerator<T> — easy to mix up by name

They sound almost identical, but they answer different questions. IEnumerable<T> asks "can I be walked through?" — a collection implements this. IEnumerator<T> asks "where are we right now in that walk-through?" — the enumerator returned by GetEnumerator() implements this. You almost never implement IEnumerator<T> directly in everyday code (as the real-world example showed, delegating to an already-enumerable field is far more common) — but understanding what it does makes foreach far less mysterious.

2. "Enumerable" doesn't mean "has a Count" or "can be indexed"

As you learned in the Collection Interfaces lesson, IEnumerable<T> is the smallest, most basic contract in the whole hierarchy. Being enumerable only promises sequential access — nothing about size, position, or mutability. Don't assume every IEnumerable<T> you receive has a Count or supports indexing; if you need those, the parameter type should say so explicitly (ICollection<T> or IList<T>).

Common Mistakes

Mistake 1 — Assuming every IEnumerable<T> has a Length or Count

Wrong — this won't compile; IEnumerable<T> makes no such promise:

void PrintCount(IEnumerable<string> items) { Console.WriteLine(items.Count); // IEnumerable<T> has no Count member }

Correct — if a count is genuinely needed, require a more specific interface:

void PrintCount(ICollection<string> items) { Console.WriteLine(items.Count); // ICollection<T> guarantees Count }

Mistake 2 — Building a custom enumerator that shares one position field on the collection itself

Storing "current position" directly on the collection class (instead of a separate enumerator, as shown in the Simple Example above) means two simultaneous foreach loops over the same instance would corrupt each other's progress. Always give each walk-through its own independent enumerator object, exactly as GetEnumerator() is meant to do.

Mistake 3 — Modifying a collection while a foreach loop is enumerating it

You saw this exact mistake in the List<T> lesson — it throws InvalidOperationException. Now you know why: most built-in enumerators detect a change to the underlying collection mid-walk-through and deliberately fail loudly, rather than silently producing incorrect or skipped results. Collect items to remove first, or loop by index backwards, as shown in that earlier lesson.

When Should I Use It?

Implement IEnumerable<T> yourself when

You usually don't need to implement it when

Rule of thumb: Most day-to-day C# code consumes IEnumerable<T> (by writing methods that accept it, as in the previous lesson) far more often than it implements it from scratch. Recognizing what it promises, and how foreach uses it, matters more at this stage than being able to hand-write enumerators fluently — that fluency comes naturally once yield return and LINQ enter the picture later in the course.

Mental Model

IEnumerable<T> = "I can hand you a way to walk through my items"
GetEnumerator() = "here's a fresh bookmark, starting before the first item"
IEnumerator<T> = the bookmark itself — MoveNext() to advance, Current to read

Remember:
· foreach is just repeated calls to MoveNext() and Current — nothing more mysterious than that.
· The enumerator is a separate object from the collection, so multiple walk-throughs never interfere with each other.
· Every collection type in this module implements IEnumerable<T> — that's the thread tying this entire module together.

Key Takeaway


Check Your Understanding

You've taken a first look at how IEnumerable<T> and foreach connect. Let's check your understanding.

1. What is the single member that the IEnumerable<T> interface requires a type to implement?

Show answer

Correct: B

Why B is correct: IEnumerable<T>'s entire contract is a single method, GetEnumerator(), which returns an IEnumerator<T> for walking through the items.

Why A is incorrect: Count belongs to ICollection<T>, a more specific interface built on top of IEnumerable<T> — not to IEnumerable<T> itself.

Why C is incorrect: MoveNext() belongs to IEnumerator<T> — the object GetEnumerator() returns, not IEnumerable<T> itself.

Why D is incorrect: Indexing belongs to IList<T>, a much more specific interface — IEnumerable<T> makes no promise about positional access at all.

Reinforcement: IEnumerable<T> is deliberately minimal — one method, which is exactly what lets so many different collection shapes implement it.

2. Roughly, what does a foreach loop get translated into by the compiler?

Show answer

Correct: B

Why B is correct: As shown in "Big Picture," the compiler mechanically rewrites foreach into exactly this pattern — get an enumerator, then loop on MoveNext()/Current until there's nothing left.

Why A is incorrect: No copying happens — foreach reads items one at a time through the enumerator, directly from the original collection.

Why C is incorrect: foreach never sorts anything, and doesn't rely on indexed access at all — many enumerable types (like HashSet<T>) have no indexer.

Why D is incorrect: foreach's behavior is entirely defined in terms of the IEnumerable<T>/IEnumerator<T> contract — that relationship is exactly what this lesson covered.

Reinforcement: Once you know this translation, foreach stops being "magic" and becomes a predictable pattern you could write by hand.

3. Why does GetEnumerator() return a brand-new enumerator object each time it's called, rather than the collection tracking its own single "current position"?

Show answer

Correct: B

Why B is correct: If "current position" lived on the collection itself, two loops over the same collection at the same time would corrupt each other's progress. A fresh enumerator per call means each walk-through gets its own independent bookmark.

Why A is incorrect: The two approaches behave very differently the moment more than one enumeration happens concurrently — this is precisely the scenario the separate-enumerator design protects against.

Why C is incorrect: Collections routinely store fields (like a backing array) — that's unrelated to why enumerators are separate objects.

Why D is incorrect: This design is about safe concurrent iteration, not about preventing changes to the data — many enumerable collections are still fully mutable.

Reinforcement: Separating "the data" from "where we currently are while walking through it" is what makes independent, simultaneous enumeration possible.

4. A custom ProductCatalog class internally stores its products in a List<Product> field. What is the simplest correct way to make ProductCatalog work with foreach?

Show answer

Correct: B

Why B is correct: As shown in the real-world WeeklySchedule example, since the internal List<Product> is already enumerable, ProductCatalog can implement IEnumerable<Product> and simply delegate: GetEnumerator() => _products.GetEnumerator();. No custom enumerator class is needed.

Why A is incorrect: This would work, but it's unnecessary extra code — delegating to the already-enumerable field accomplishes the exact same result far more simply.

Why C is incorrect: Any type — built-in or custom — can be used with foreach as soon as it implements IEnumerable<T>, exactly as this lesson demonstrated with NameList.

Why D is incorrect: A Count property alone satisfies no enumeration contract at all — foreach specifically needs GetEnumerator(), which IEnumerable<T> provides.

Reinforcement: Delegating to an already-enumerable internal field is the most common, simplest way real-world code implements IEnumerable<T>.

You've completed Part IV — Collections and Data. You now understand arrays, multidimensional and jagged arrays, List<T>, Dictionary<TKey,TValue>, HashSet<T>, Stack<T> and Queue<T>, the interface hierarchy that ties them together, and the first-look mechanics of IEnumerable<T> that make foreach — and eventually LINQ — possible.


dotnetmadeeasy.com — Learn C# and .NET, the right way.