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.
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.
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
}IEnumerable<T> — the collection's job: "I can hand out an enumerator whenever you ask."IEnumerator<T> — the enumerator's job: "I remember exactly where we are in the walk-through, and I can move forward one step at a time."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.
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.
foreach (string name in names)
Console.WriteLine(name);
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.
foreach calls GetEnumerator() exactly once, at the start, getting back a fresh enumerator positioned before the first item.foreach calls MoveNext(). If there's another item, the enumerator advances and MoveNext() returns true. If there's nothing left, it returns false and the loop ends.MoveNext() returned true, foreach reads Current to get the item at the enumerator's new position, and assigns it to your loop variable (name, in the example above).MoveNext() finally returns false, the loop exits automatically — you never write this bookkeeping yourself.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
// CaraCode → Meaning → Result:
NameList doesn't do the walking itself — it just knows how to produce a fresh NameEnumerator whenever asked, via GetEnumerator().NameEnumerator is the one that actually remembers position: it starts at -1 (before the first item), and each MoveNext() call steps forward by one, reporting false once it runs off the end.NameList implements IEnumerable<string>, it can be used in a foreach exactly like an array or a List<T> — no special casing anywhere in the language.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: CaraNotice 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.
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.
foreach isn't magic built directly into the runtime for each collection type — the compiler simply rewrites it into calls to GetEnumerator(), MoveNext(), and Current. Any type offering those (via IEnumerable<T>) automatically works.foreach loops over the same list running at once — they'd fight over one shared position. By making the enumerator its own small object, each call to GetEnumerator() hands out an independent bookmark, so nested or parallel loops over the same collection work safely.Where, Select, OrderBy, and dozens more) works by consuming an IEnumerable<T> and producing another one — which is exactly why LINQ works identically across arrays, lists, dictionaries, and any custom type you make enumerable, including the NameList you just built.yield return keyword lets you write an enumerator's logic as ordinary-looking code, and the compiler generates the enumerator class for you behind the scenes. That mechanism, along with the full depth of LINQ, is covered later in this course, once you have more collection experience to build on.IEnumerable<T> vs IEnumerator<T> — easy to mix up by nameThey 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.
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>).
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
} 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.
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.
foreach (and later, LINQ).WeeklySchedule example) while still exposing a clean, iterable surface.List<T>, Dictionary<TKey,TValue>, etc.) — they already implement it for you.IReadOnlyList<T>) instead of wrapping a whole new custom type.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.
MoveNext() to advance, Current to readforeach is just repeated calls to MoveNext() and Current — nothing more mysterious than that.IEnumerable<T> — that's the thread tying this entire module together.
IEnumerator<T> via GetEnumerator().foreach is compiler sugar — it's mechanically rewritten into calls to GetEnumerator(), then repeated MoveNext()/Current until MoveNext() returns false.IEnumerable<T> on a custom type usually just means delegating to an already-enumerable field, as shown in the real-world example.yield return and LINQ, both built directly on top of what you learned here, are covered in depth later in the course.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?
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?
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"?
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?
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.