In this article, we are going to learn about IEnumerable in C#. IEnumerable acts as an abstraction over a collection and allows us to iterate over it without knowing the actual type of the collection.
Let’s begin.
What is the IEnumerable Interface in C#?
The IEnumerable
interface is the base for all the non-generic collections that can be enumerated.
It exposes a single method GetEnumerator()
. This method returns a reference to yet another interface System.Collections.IEnumerator
.
The IEnumerator
interface in turn provides the ability to iterate through a collection by using the methods MoveNext()
and Reset()
, and the property Current
.
Hence, in order for us to be able to use ForEach
over a collection, it must implement IEnumerable
.
Extension Methods in IEnumerable
The IEnumerable
interface includes some extension methods along with the GetEnumerator()
method.
We use deferred execution to implement these methods, so the actual value of the return object is realized only when the object is enumerated either using a ForEach
loop or by calling its GetEnumerator()
method.
Cast<TResult>
This method takes a sequence of type IEnumerable
and converts all the objects of the given sequence to the type TResult
, thus resulting in a return type IEnumerable<TResult>
.
The Cast<TResult>()
method is a generic method where TResult
acts as a placeholder for the actual type we want to work with at compile time. It allows the use of standard LINQ query operators like filtering, sorting, etc. on a non-generic type such as Arraylist
:
var primes = new ArrayList { 5, 3, 2, 7 }; var primeQuery = primes.Cast<int>().OrderBy(prime => prime).Select(prime => prime); var expected = new List<int> { 2, 3, 5, 7 }; Assert.Equal(expected, primeQuery);
We are able to convert a non-generic collection of type Arraylist
into IEnumerable<int>
and thus have all its functionalities available.
OfType<TResult>
This method takes a sequence of type IEnumerable
and filters it based on the specified type. It returns only those elements from the input source that can be cast to the type TResult
:
var cities = new ArrayList { "London", "Paris", "Madrid", "Berlin", 7, "Lisbon" }; var cityQuery = cities.OfType<string>(); var expected = new List<string> { "London", "Paris", "Madrid", "Berlin", "Lisbon" }; Assert.Equal(expected, cityQuery);
This is where it differs from Cast<TResult>()
which instead throws an exception if an element can’t be cast to TResult
. We can also use standard query operators such as Where()
after filtering the source:
var cities = new ArrayList { "London", "Paris", "Madrid", "Berlin", 7, "Lisbon", 8, 12 }; var evens = cities.OfType<int>().Where(x => x % 2 == 0); var expected = new List<int> { 8, 12 }; Assert.Equal(expected, evens);
AsParallel Method
This method enables the parallelization of a query. It takes a sequence of type IEnumerable
and converts it to the type ParallelQuery:
var numbers = Enumerable.Range(0, 100); var oddsCount = numbers.AsParallel().Count(x => x % 2 != 0); var expected = 50; Assert.Equal(expected, oddsCount);
The AsParallel()
method binds the query to PLINQ or Parallel LINQ. The PLINQ queries work in a similar way to the sequential LINQ queries where they operate on in-memory data sources and allow deferred execution.
However, they attempt to make use of all available processors by partitioning the data source into segments and then executing the query on each segment on separate worker threads in parallel on multiple processors.
AsQueryable Method
This method takes a sequence of type IEnumerable
and converts it into IQueryable:
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; var query = numbers.AsQueryable(); var multiplesOf3 = query.Where(x => x % 3 == 0); var expected = new List<int> { 3, 6, 9 }; Assert.Equal(expected, multiplesOf3); Assert.Equal("Constant", query.Expression.NodeType.ToString());
The IQueryable
interface extends the IEnumerable
interface and hence is similar in functionality.
The main difference between both is that while IEnumerable
is more suited to collections loaded using LINQ where filtering is required on the client side where IEnumerable
is, IQueryable
creates a SQL Query on the server side and only filtered data is sent to the client side.
This concludes the extension methods of IEnumerable in C#. Let’s now take a look at implementing a custom IEnumerable interface in code.
Code Implementation of IEnumerable Interface
Let’s start by creating a Book
class:
public class Book { public Book(string name, string author) { Name = name; Author = author; } public string Name { get; set; } public string Author { get; set; } }
Now, that we have our business object, let’s create a collection of this object:
public class Library : IEnumerable { private readonly Book[] _books; public Library(Book[] books) { _books = new Book[books.Length]; for (int i = 0; i < books.Length; i++) { _books[i] = books[i]; } } }
The class Library
implements IEnumerable
so that we may use the ForEach
loop on the class objects. However, the code does not compile until we implement the GetEnumerator()
method of the interface.
As we need to implement IEnumerator
when implementing IEnumerable
, let’s first implement the IEnumerator
interface:
public class BookEnumerator : IEnumerator { private readonly Book[] _books; private int _index; public BookEnumerator(Book[] books) { _books = books; } public Book Current { get { return _books[_index]; } } object IEnumerator.Current => Current; public bool MoveNext() { _index++; return _index < _books.Length; } public void Reset() { _index = -1; } }
We start by implementing the required members of IEnumerator
in the class BookEnumerator
i.e. the property Current
, and the methods Reset()
and MoveNext()
.
When the class is instantiated, the enumerator _index
is placed before the first element of the collection. When iterating over the collection, we send a call to MoveNext()
that advances the enumerator to the first position.
The Current
property indicates the position of the enumerator at any point. It returns the same value until MoveNext()
or Reset()
are called. It is undefined under these conditions:
- When the enumerator places before the first element of the collection, immediately after the instantiation
- When the last call to
MoveNext()
returns false i.e. the iteration of the collection is complete - If any change in the collection such as addition, modification, etc. invalidates the enumerator
The Reset()
method resets the enumerator position to the original. The MoveNext()
method advances _index
till it reaches the position after the last element of the collection. After this, the MoveNext()
method always returns false
.
With this step done, we are now ready to revisit our Library
class and get rid of the compiler errors due to not implementing the GetEnumerator()
method:
public class Library : IEnumerable { //Existing Code excluded for brevity public BookEnumerator GetEnumerator() => new BookEnumerator(_books); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); }
Now let’s see the Library
class in action as an implementation of IEnumerable
:
var books = new Book[5] { new Book("Anna Karenina", "Leo Tolstoy"), new Book("To Kill a Mockingbird", "Harper Lee"), new Book("The Great Gatsby", "F. Scott Fitzgerald"), new Book("One Hundred Years of Solitude", "Gabriel García Márquez"), new Book("A Passage to India", "E.M. Forster") }; var library = new Library(books); foreach (var book in library) { Console.WriteLine($"Book: {book.Name}, Author: {book.Author}"); }
The IEnumerable interface allows the use of ForEach
loop on the collection of type Library
. On running the program we receive the expected output:
Book: Anna Karenina, Author: Leo Tolstoy Book: To Kill a Mockingbird, Author: Harper Lee Book: The Great Gatsby, Author: F. Scott Fitzgerald Book: One Hundred Years of Solitude, Author: Gabriel García Márquez Book: A Passage to India, Author: E.M. Forster
Conclusion
In this article, we learned about IEnumerable in C#. We learned how it is necessary to implement for us to iterate over a collection. We learned the associated extension methods and their implementations.