ZetCode

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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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

IEnumerable<T> interface

In this article we have covered the C# IEnumerable interface.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.