C# IEnumerable
last modified November 6, 2026
In this article we explain the C# IEnumerable interface.
IEnumerable<T> is a standard .NET interface that represents
a sequence of elements which can be iterated with the foreach
loop. Arrays, lists, sets, dictionaries, strings, and LINQ query results all
implement it.
The interface has a single method, GetEnumerator, which returns
an IEnumerator<T>. The enumerator does the actual
iteration: its MoveNext method advances to the next element, the
Current property returns the current element, and the
Reset method returns to the start. (Reset is
optional; many enumerators throw NotSupportedException.)
IEnumerator<T> also implements IDisposable.
In addition, IEnumerable enables generators in C#, using the
yield return and yield break statements, and it is
the foundation of LINQ to Objects.
Enabling foreach iterations
IEnumerable enables seamless interaction with the
foreach keyword.
IEnumerable<int> range = Enumerable.Range(1, 6);
List<string> words = ["sky", "cup", "beer"];
IEnumerable<char> chars = "an old falcon";
decimal[] vals = [1m, 2m, 3m];
HashSet<int> set = [3, 1, 2];
DoIteration(range);
DoIteration(words);
DoIteration(chars);
DoIteration(vals);
DoIteration(set);
void DoIteration<T>(IEnumerable<T> vals)
{
foreach (var e in vals)
{
Console.Write($"{e} ");
}
Console.WriteLine();
}
We define a generic DoIteration method that iterates over a
range of numbers, a list of words, a sequence of characters, an array of
decimals, and a set of integers.
IEnumerable<char> chars = "an old falcon";
A string implements IEnumerable<char>, so it can be
assigned directly.
void DoIteration<T>(IEnumerable<T> vals)
The DoIteration method takes a generic IEnumerable
as a parameter. It accepts any sequence, regardless of the concrete
collection type.
$ dotnet run 1 2 3 4 5 6 sky cup beer a n o l d f a l c o n 1 2 3 3 1 2
How foreach works
The foreach statement is a shortcut for working with the
enumerator. The following example does the same work manually.
List<string> words = ["sky", "cup", "beer"];
using IEnumerator<string> it = words.GetEnumerator();
while (it.MoveNext())
{
Console.WriteLine(it.Current);
}
Console.WriteLine(it.MoveNext());
We iterate over a list of words using its enumerator.
using IEnumerator<string> it = words.GetEnumerator();
We get the enumerator from the list. The using declaration
disposes it at the end of the scope, just like foreach does.
while (it.MoveNext())
{
Console.WriteLine(it.Current);
}
The enumerator starts before the first element. Each MoveNext
call advances to the next element and returns true; when there
are no more elements, it returns false.
$ dotnet run sky cup beer False
IEnumerable and generators
The easiest way to produce an IEnumerable is a generator method
with yield return. The C# yield
article covers generators in more detail.
IEnumerable<int> evens = EvenNumbers(20);
Console.WriteLine(string.Join(" ", evens));
IEnumerable<string> lines = Countdown(3);
Console.WriteLine(string.Join(", ", lines));
IEnumerable<int> EvenNumbers(int max)
{
for (int i = 0; i <= max; i += 2)
{
yield return i;
}
}
IEnumerable<string> Countdown(int from)
{
for (int i = from; i > 0; i--)
{
yield return i.ToString();
}
yield return "liftoff";
}
We have two generator methods.
for (int i = 0; i <= max; i += 2)
{
yield return i;
}
The EvenNumbers method returns even numbers up to the given
maximum. The compiler generates the enumerator class for us.
yield return "liftoff";
A generator can contain several yield return statements; after
the loop, the Countdown method returns one more value.
$ dotnet run 0 2 4 6 8 10 12 14 16 18 20 3, 2, 1, liftoff
IEnumerable with a custom class
In the next example we create an enumerable custom class.
using System.Collections;
Bag bag = new();
foreach (object e in bag)
{
Console.WriteLine(e);
}
Console.WriteLine(bag.Count());
class Bag : IEnumerable<object>
{
private readonly object[] data = [1, "falcon", 4.5, new DateOnly(2026, 1, 31)];
public IEnumerator<object> GetEnumerator()
{
foreach (var item in data)
{
yield return item;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
We define a Bag class which can be traversed with a
foreach loop.
public IEnumerator<object> GetEnumerator()
{
foreach (var item in data)
{
yield return item;
}
}
The GetEnumerator method generates the elements of the
data array with yield return.
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
IEnumerable<T> inherits from the older non-generic
IEnumerable interface. We implement its method explicitly and
delegate to the generic version.
Console.WriteLine(bag.Count());
Since the class implements IEnumerable<object>, all LINQ
methods are available for it.
$ dotnet run 1 falcon 4.5 1/31/2026 4
Custom enumerator
Instead of yield return, we can write the enumerator class by
hand. This shows what the compiler generates for us.
using System.Collections;
var range = new StepRange(1, 10, 3);
foreach (var e in range)
{
Console.Write($"{e} ");
}
Console.WriteLine();
Console.WriteLine(range.Sum());
class StepRange(int start, int end, int step) : IEnumerable<int>
{
public IEnumerator<int> GetEnumerator() => new StepEnumerator(start, end, step);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private sealed class StepEnumerator(int start, int end, int step) : IEnumerator<int>
{
private int current = start - step;
public int Current => current;
object IEnumerator.Current => Current;
public bool MoveNext()
{
if (current + step > end)
{
return false;
}
current += step;
return true;
}
public void Reset() => current = start - step;
public void Dispose() { }
}
}
The StepRange class represents integers from start to end with
a given step.
private int current = start - step;
The enumerator starts one step before the first element, since the first
MoveNext call must move to the first element.
public bool MoveNext()
{
if (current + step > end)
{
return false;
}
current += step;
return true;
}
The MoveNext method advances the position and reports whether
an element is available.
public void Dispose() { }
The enumerator holds no resources, so Dispose does nothing.
$ dotnet run 1 4 7 10 22
IEnumerable with query expressions
LINQ query expressions return IEnumerable objects, which can be
iterated later with foreach.
IEnumerable<int> vals = from value in Enumerable.Range(0, 5)
select value * value;
Console.WriteLine(string.Join(' ', vals));
List<string> words = [ "sky", "forest", "war", "buy",
"crypto", "cup", "water", "cloud" ];
IEnumerable<string> res = from word in words
where word.StartsWith('c')
select word;
Console.WriteLine(string.Join(',', res));
words.Add("coin");
Console.WriteLine(string.Join(',', res));
We work with two query expressions: a range of integers and a list of words.
words.Add("coin");
Console.WriteLine(string.Join(',', res));
A query is not a collection of results; it is a recipe that runs each time it is enumerated. After we add a new word to the list, the same query returns it as well.
$ dotnet run 0 1 4 9 16 crypto,cup,cloud crypto,cup,cloud,coin
Multiple enumeration
Because a lazy IEnumerable is recomputed on each enumeration,
calling several LINQ methods on it repeats the work.
int calls = 0;
IEnumerable<int> data = Enumerable.Range(1, 5).Select(e =>
{
calls++;
return e * 10;
});
Console.WriteLine(data.Count());
Console.WriteLine(data.Sum());
Console.WriteLine(data.First());
Console.WriteLine($"selector calls: {calls}");
calls = 0;
List<int> list = data.ToList();
Console.WriteLine(list.Count);
Console.WriteLine(list.Sum());
Console.WriteLine(list[0]);
Console.WriteLine($"selector calls: {calls}");
We count how many times the selector function runs.
Console.WriteLine(data.Count()); Console.WriteLine(data.Sum()); Console.WriteLine(data.First());
Each of the three calls enumerates the sequence again. The selector runs eleven times for five elements.
List<int> list = data.ToList();
The ToList method enumerates the sequence once and stores the
results. When we need the data several times, or when the sequence is
expensive to produce (e.g. a database query), we materialize it first.
$ dotnet run 5 150 10 selector calls: 11 5 150 10 selector calls: 5
Covariance and empty sequences
IEnumerable<T> is covariant: it is declared as
IEnumerable<out T>. A sequence of a derived type can be
used where a sequence of a base type is expected.
List<string> words = ["sky", "cup", "beer"];
IEnumerable<object> objects = words;
foreach (var e in objects)
{
Console.WriteLine($"{e} ({e.GetType().Name})");
}
IEnumerable<int> empty = Enumerable.Empty<int>();
Console.WriteLine(empty.Any());
IEnumerable<string> FindWords(string prefix) =>
words.Where(w => w.StartsWith(prefix));
Console.WriteLine(string.Join(",", FindWords("c")));
Console.WriteLine(FindWords("x").Any());
We use a list of strings as a sequence of objects.
IEnumerable<object> objects = words;
This assignment is allowed because a string is an object and the sequence is
read-only. The same conversion would not compile for
List<object>.
IEnumerable<int> empty = Enumerable.Empty<int>();
The Enumerable.Empty method returns a cached empty sequence.
IEnumerable<string> FindWords(string prefix) =>
words.Where(w => w.StartsWith(prefix));
Returning IEnumerable<T> from a method hides the
concrete collection type and lets the caller only read the elements. A method
returning a sequence should return an empty sequence rather than
null when there are no results.
$ dotnet run sky (String) cup (String) beer (String) False cup False
Source
In this article we have covered the C# IEnumerable interface.
Author
List all C# tutorials.