IEnumerable vs IEnumerator in C#: Key Differences
c# ienumerable vs ienumerator: Understand the difference between IEnumerable and IEnumerator in C#, how foreach uses both interfaces, and when to implement each one di...
When developers search for c# ienumerable vs ienumerator, they are usually trying to understand why two interfaces exist for what looks like one operation: iterating a collection. The short answer is that IEnumerable<T> describes the capability to be enumerated, while IEnumerator<T> is the stateful object that performs the enumeration.
What IEnumerable and IEnumerator Actually Represent
IEnumerable<T> is a contract. It says: "this object can produce a sequence of elements." It does not hold any information about where you are in that sequence. You can call GetEnumerator() on it repeatedly, and each call returns a fresh enumerator positioned before the first element.
IEnumerator<T> is the active cursor. It holds the current position, exposes the current element through the Current property, and advances through the sequence when you call MoveNext(). It is mutable state, and it is single-use in practice.
The Interface Members
public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); } public interface IEnumerator<out T> : IDisposable, IEnumerator { T Current { get; } bool MoveNext(); void Reset(); }
IEnumerable<T> has exactly one method: GetEnumerator(). The non-generic IEnumerable interface also declares GetEnumerator(), but returns the non-generic IEnumerator. Most collection types implement both.
IEnumerator<T> extends IDisposable, which matters because many enumerators hold resources such as open file handles or database connections. The Reset() method exists for legacy compatibility, but most modern implementations throw NotSupportedException when you call it.
How foreach Uses Both Interfaces
When you write:
foreach (var item in collection) { Console.WriteLine(item); }
The compiler expands this into code that calls GetEnumerator() on the collection, then repeatedly calls MoveNext() and reads Current until the sequence ends:
var enumerator = collection.GetEnumerator(); try { while (enumerator.MoveNext()) { var item = enumerator.Current; Console.WriteLine(item); } } finally { if (enumerator is IDisposable disposable) { disposable.Dispose(); } }
This expansion shows the division of labor. The collection provides the enumerator; the loop drives it. If you ever need to iterate manually without foreach, you write this same pattern yourself.
Deferred Execution and When Each Interface Runs
LINQ queries return IEnumerable<T> that have not executed yet. The query is a description of work to be done. Execution starts only when something calls GetEnumerator() and begins calling MoveNext().
IEnumerable<int> query = numbers.Where(n => n > 10); // No filtering has happened yet. foreach (var n in query) { // The filter runs here, one element at a time. Console.WriteLine(n); }
The IEnumerator<T> is what drives that execution. Each MoveNext() call pulls the next element through the LINQ pipeline. This is why the same IEnumerable<T> can be enumerated multiple times: each enumeration creates a new enumerator and runs the pipeline again.
State and Lifetime: Why IEnumerator Is Not Reusable
An IEnumerator<T> is a one-way cursor. Once MoveNext() returns false, the enumerator is exhausted. Calling MoveNext() again keeps returning false, and reading Current after exhaustion is undefined behavior in most implementations.
Because of this, you cannot rewind an enumerator. To iterate the same sequence again, you must call GetEnumerator() on the original IEnumerable<T> and get a new enumerator. This is a common source of confusion when developers try to reuse an enumerator across two loops.
Performance Considerations
When you iterate a List<T> directly with foreach, the compiler uses the struct enumerator List<T>.Enumerator. This avoids heap allocation because the enumerator lives on the stack.
List<int> list = GetList(); // Uses List<T>.Enumerator directly - no heap allocation. foreach (var item in list) { } IEnumerable<int> sequence = list; // Boxes the struct enumerator - allocates on each iteration. foreach (var item in sequence) { }
When you cast the list to IEnumerable<T>, the struct enumerator is boxed, and each GetEnumerator() call allocates an object on the heap. In a hot loop processing millions of items, this allocation overhead is measurable. In typical application code, it rarely matters.
The same principle applies to arrays. int[] has a struct enumerator too, but the array's enumerator is a simple index-based cursor.
When to Implement These Interfaces Directly
Most developers never write an IEnumerator<T> implementation by hand. You implement IEnumerable<T> by using iterator methods with yield return, and the compiler generates both interfaces for you.
public IEnumerable<int> GetPositiveNumbers(IEnumerable<int> source) { foreach (var n in source) { if (n > 0) { yield return n; } } }
The compiler builds a hidden state machine that implements IEnumerable<T>, IEnumerator<T>, and IDisposable. The state machine tracks the current position and the local variables across MoveNext() calls.
You need a manual IEnumerator<T> implementation only when yield cannot express the iteration logic, or when you must control disposal behavior precisely. For example, a custom enumerator that reads from a stream and must close the stream on Dispose().
Common Pitfall: Multiple Enumeration
Because IEnumerable<T> can be enumerated multiple times, each enumeration runs the underlying logic again. If the source is a database query or an expensive computation, enumerating twice executes the work twice.
IEnumerable<int> sequence = GetExpensiveSequence(); var count = sequence.Count(); // Runs the sequence once. var sum = sequence.Sum(); // Runs it again.
If you need to iterate the same data more than once, materialize it first:
var materialized = GetExpensiveSequence().ToList(); var count = materialized.Count; var sum = materialized.Sum();
This trades a single upfront cost for predictable behavior and avoids recomputing the sequence.
Choosing Between IEnumerable and IEnumerator in Your Code
Use IEnumerable<T> as a parameter or return type when you want to express "this can be iterated" without committing to a specific collection type. This is the right default for most APIs.
Use IEnumerator<T> when you need to control iteration manually. A classic example is merging two sorted sequences, where you must advance each enumerator independently:
public static IEnumerable<int> MergeSorted( IEnumerator<int> left, IEnumerator<int> right) { bool hasLeft = left.MoveNext(); bool hasRight = right.MoveNext(); while (hasLeft && hasRight) { if (left.Current <= right.Current) { yield return left.Current; hasLeft = left.MoveNext(); } else { yield return right.Current; hasRight = right.MoveNext(); } } while (hasLeft) { yield return left.Current; hasLeft = left.MoveNext(); } while (hasRight) { yield return right.Current; hasRight = right.MoveNext(); } }
Here, IEnumerator<T> is the correct parameter type because the merge algorithm needs to peek at both sequences and advance them at different rates. Passing IEnumerable<T> would force the caller to manage two separate loops and would obscure the stateful nature of the operation.