Skip to main content

C# IEnumerable Interface

Introduction

The IEnumerable interface is one of the most fundamental interfaces in C#'s collection framework. It provides a standard way to iterate through a collection of objects, enabling foreach loops and LINQ operations. Understanding IEnumerable is essential for any C# developer as it forms the foundation for collection manipulation in .NET applications.

In this tutorial, you'll learn:

  • What the IEnumerable interface is
  • How to use and implement the IEnumerable interface
  • The relationship between IEnumerable and IEnumerator
  • Real-world applications and benefits
  • Generic version IEnumerable<T>
  • How LINQ works with IEnumerable

What is the IEnumerable Interface?

IEnumerable is defined in the System.Collections namespace and is the base interface for all non-generic collections in C# that can be enumerated. Its generic counterpart, IEnumerable<T>, is found in the System.Collections.Generic namespace.

Here's the definition of the IEnumerable interface:

csharp
public interface IEnumerable
{
IEnumerator GetEnumerator();
}

And its generic version:

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

The sole purpose of IEnumerable is to provide an IEnumerator object through its GetEnumerator() method, which enables traversal through a collection.

IEnumerable vs IEnumerator

To fully understand IEnumerable, we need to understand its relationship with IEnumerator:

  • IEnumerable: Represents a collection that can be enumerated. It's the "enumerable" collection itself.
  • IEnumerator: Represents the cursor or pointer that moves through the collection. It keeps track of the current position during enumeration.

Here's the definition of IEnumerator:

csharp
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}

When you use a foreach loop in C#, the compiler implicitly uses these interfaces:

  1. It calls GetEnumerator() on the collection (which implements IEnumerable)
  2. It repeatedly calls MoveNext() on the returned IEnumerator
  3. It accesses the Current property to get each element

Using IEnumerable

Most collection types in .NET (like arrays, List<T>, Dictionary<TKey, TValue>) already implement IEnumerable, so you can use them directly with foreach loops:

csharp
// Using IEnumerable with built-in collections
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };

// Using the foreach loop to iterate through an IEnumerable
foreach (string name in names)
{
Console.WriteLine(name);
}

Output:

Alice
Bob
Charlie

You can also explicitly use the IEnumerator to iterate through collections:

csharp
string[] fruits = { "Apple", "Banana", "Cherry" };
IEnumerator enumerator = fruits.GetEnumerator();

while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}

Output:

Apple
Banana
Cherry

Implementing IEnumerable

Let's create a custom class that implements IEnumerable to understand how it works internally. We'll create a simple NumberRange class that represents a range of integers:

csharp
using System;
using System.Collections;
using System.Collections.Generic;

public class NumberRange : IEnumerable<int>
{
private readonly int _start;
private readonly int _end;

public NumberRange(int start, int end)
{
_start = start;
_end = end;
}

// Implement the generic GetEnumerator method
public IEnumerator<int> GetEnumerator()
{
for (int i = _start; i <= _end; i++)
{
yield return i;
}
}

// Implement the non-generic GetEnumerator method required by IEnumerable
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

// Usage:
class Program
{
static void Main()
{
NumberRange range = new NumberRange(1, 5);

// Using foreach with our custom IEnumerable
Console.WriteLine("Numbers in range:");
foreach (int number in range)
{
Console.WriteLine(number);
}
}
}

Output:

Numbers in range:
1
2
3
4
5

Notice the use of the yield return statement - this is a C# feature that simplifies the creation of iterators. It automatically handles the implementation of IEnumerator for us.

The yield Keyword

The yield keyword is a powerful feature in C# that simplifies implementing enumerators. When the compiler encounters a yield return statement, it generates a state machine to manage the enumeration process.

Benefits of using yield return:

  • Simplifies implementation of IEnumerable
  • Enables lazy evaluation (items are only generated when requested)
  • Reduces memory usage for large collections

Here's a simple example:

csharp
public IEnumerable<int> GenerateEvenNumbers(int max)
{
for (int i = 0; i <= max; i += 2)
{
Console.WriteLine($"Generating {i}");
yield return i;
}
}

// Usage:
foreach (int number in GenerateEvenNumbers(10))
{
Console.WriteLine($"Using {number}");
}

Output:

Generating 0
Using 0
Generating 2
Using 2
Generating 4
Using 4
Generating 6
Using 6
Generating 8
Using 8
Generating 10
Using 10

Notice how the "Generating" and "Using" messages are interleaved. This shows that each number is generated only when needed by the foreach loop!

IEnumerable and LINQ

One of the most powerful aspects of IEnumerable is its integration with LINQ (Language Integrated Query). LINQ provides a rich set of extension methods to query and transform data in collections.

Here's a simple example:

csharp
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Using LINQ with IEnumerable
var evenNumbers = numbers.Where(n => n % 2 == 0);
var squaredNumbers = numbers.Select(n => n * n);

Console.WriteLine("Even numbers:");
foreach (int num in evenNumbers)
{
Console.WriteLine(num);
}

Console.WriteLine("\nSquared numbers:");
foreach (int num in squaredNumbers)
{
Console.WriteLine(num);
}

Output:

Even numbers:
2
4
6
8
10

Squared numbers:
1
4
9
16
25
36
49
64
81
100

LINQ operations are deferred, meaning they aren't executed until you actually enumerate the results. This is called "lazy evaluation" and is one of the key benefits of the IEnumerable pattern.

Real-World Applications

1. Building Custom Collections

Custom collections that implement IEnumerable integrate seamlessly with the rest of C#'s collections framework:

csharp
public class StudentCollection : IEnumerable<Student>
{
private List<Student> _students = new List<Student>();

public void Add(Student student)
{
_students.Add(student);
}

public IEnumerator<Student> GetEnumerator()
{
return _students.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

// Usage:
var students = new StudentCollection();
students.Add(new Student("Alice", 95));
students.Add(new Student("Bob", 85));
students.Add(new Student("Charlie", 90));

// Can use with foreach
foreach (var student in students)
{
Console.WriteLine($"{student.Name}: {student.Grade}");
}

// Can use with LINQ
var topStudents = students.Where(s => s.Grade >= 90);

2. Implementing Data Streaming

IEnumerable can be used to process large data streams efficiently:

csharp
public static IEnumerable<string> ReadLinesFromFile(string filePath)
{
using (StreamReader reader = new StreamReader(filePath))
{
string line;
while ((line = reader.ReadLine()) != null)
{
yield return line;
}
}
}

// Usage:
foreach (string line in ReadLinesFromFile("largefile.txt"))
{
if (line.Contains("ERROR"))
{
Console.WriteLine($"Found error: {line}");
}
}

This processes the file line by line without loading the entire file into memory.

3. Creating Infinite Sequences

IEnumerable with yield return allows for creating infinite sequences:

csharp
public static IEnumerable<int> Fibonacci()
{
int a = 0, b = 1;

while (true)
{
yield return a;
int temp = a;
a = b;
b = temp + b;
}
}

// Usage (we need to limit the sequence ourselves):
int count = 0;
foreach (int num in Fibonacci())
{
Console.WriteLine(num);
count++;

if (count >= 10) break;
}

Output:

0
1
1
2
3
5
8
13
21
34

Best Practices

  1. Use IEnumerable<T> for method returns when:

    • The method returns a sequence of items
    • The caller only needs to iterate through the collection
    • You want to enable deferred execution
  2. Don't use IEnumerable<T> when:

    • The caller needs random access to elements (use IList<T> instead)
    • The caller needs to modify the collection (use appropriate collection interface)
    • The caller needs to check collection size frequently (use IReadOnlyCollection<T>)
  3. Avoid multiple enumeration of the same IEnumerable if it's expensive to generate:

csharp
// Bad: Enumerates the expensive operation twice
var numbers = ExpensiveNumberOperation();
int count = numbers.Count(); // First enumeration
if (count > 0)
{
var first = numbers.First(); // Second enumeration
}

// Good: Save the results if you need to enumerate more than once
var numbersList = ExpensiveNumberOperation().ToList();
int betterCount = numbersList.Count;
if (betterCount > 0)
{
var betterFirst = numbersList[0];
}

Summary

The IEnumerable interface is a cornerstone of C#'s collection framework that provides a standard way to iterate through collections. Key takeaways:

  • IEnumerable defines a single method GetEnumerator() that returns an IEnumerator object
  • It enables the foreach loop syntax in C#
  • The yield keyword simplifies implementation of custom enumerables
  • IEnumerable forms the foundation for LINQ operations
  • It enables lazy evaluation and efficient processing of sequences

By understanding and leveraging IEnumerable, you can create more efficient, flexible, and maintainable code, especially when working with collections and data processing.

Practice Exercises

  1. Create a custom IEnumerable<T> class that generates a sequence of dates between two given dates.
  2. Implement a custom IEnumerable<T> collection that represents a circular buffer.
  3. Create a method that uses yield return to generate the first N prime numbers.
  4. Build a custom IEnumerable<T> that can merge and sort multiple lists.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)