How to implement IEnumerable interface?
We’re gonna try to answer the above question with a series of subquestions. Hopefully, by the end of this article, we’ll have a better understanding of IEnumerable interface.
Why should we implement IEnumerable interface?
To provide a simpler way to enumerate our user-defined types.
Let’s say we have a custom class called “OddNumbers” whose sole purpose is to encapsulate an array of the first n odd numbers.
Now, if we needed to iterate through the array of odd numbers outside this class, we’d have to expose the internal array by providing a method or property which returns the array.
This is fine in some scenarios, but what if we didn’t want to expose the internal array? Does there still exist a way to iterate through the array?
This is where IEnumerable interface comes in.
How to implement IEnumerable interface?
We can have our class implement the IEnumerable interface which tells the compiler that an instance of our class can be iterated upon via the foreach loop.
Implementing the IEnumerable interface requires us to provide an implementation of the GetEnumerator() method. This method returns an IEnumerator object which is the handle used to iterate upon an instance of our class.
And this is pretty much it!! This covers the basics of how to implement IEnumerable interface.
But let’s dig a bit deeper to understand more.
Implementing IEnumerable vs IEnumerable<T> interface
In the previous example, our class implemented the non-generic IEnumerable interface. As a result, the GetEnumerator() method returned an instance of the non-generic IEnumerator.
Had our class implemented the generic IEnumerable<T> interface, the GetEnumerator() method would have had to return an instance of the generic IEnumerator<T>.
So what's the difference and which is better to use?
When the return type is non-generic IEnumerator, we don’t really specify what the data type of each member of the sequence being iterated upon is. Hence it defaults to object. This is fine as long as we know the data type when iterating via the foreach loop.
But if we didn’t know this information and used var, things can go wrong.
error CS0019: Operator ‘*’ cannot be applied to operands of type ‘object’ and ‘int’
We can fix the above by having our class implement the generic IEnumerable<T> interface
Now using var in foreach to iterate over the sequence is fine.
var can now infer the type to be int rather than object.
So it's always just better to implement the IEnumerable<T> interface since it's type-safe.
Implementing IEnumerable interface is not absolutely necessary
This might sound weird after all the talk about implementing IEnumerable. But it's important we understand this before moving on. It's not absolutely necessary for our class to implement IEnumerable (or IEnumerable<T>) interface for foreach to iterate upon an instance of our class. When foreach tries to iterate upon an instance of our class, it looks for the GetEnumerator() method in our class, which returns an IEnumerator instance. That is the sole requirement, not implementing the IEnumerable interface.
We could very well have our class not implement IEnumerable but provide an implementation of IEnumerator GetEnumerator(); method and we could still use foreach to iterate over an instance of our class.
That said, it's always a good idea for our classes to implement IEnumerable (or IEnumerable<T>) interface because that makes the intent clear and forces devs to implement the GetEnumerator() method rather than having to remember doing so.
Another reason to implement IEnumerable is to be able to pass instances of our class to methods that expect an IEnumerable. That’s always convenient.
What's “yield return” in our GetEnumerator() method?
Something we need to understand here is that using yield in our GetEnumerator() method is not an absolute necessity. The main requirement of this method is to return an IEnumerator instance. We could create that manually (as we’ll see in a bit). The yield keyword just makes the C# compiler do that for us.
Using yield to define an iterator removes the need for an explicit extra class (the class that holds the state for an enumeration) when you implement the IEnumerable and IEnumerator pattern for a custom collection type.
The “explicit extra class” that they mention above is what we’re looking at next.
Manually creating an enumerator
Create another class called “OddNumberEnumerator” and have it implement the IEnumerator<T> interface. This tells the C# compiler that an instance of this class can act as a handle that can be used to iterate a sequence.
The iteration related logic itself is expressed via the implementation of the MoveNext() method and “Current” property.
The “Current” read only property holds the current element being looked at in the iteration process.
The MoveNext() returns a bool notifying whether we can move ahead in the iteration or not. If we can move ahead in the iteration, it also updates the value of the “Current” property.
Here’s the class in its entirety
All that’s left now is to update the GetEnumerator() method in our “OddNumbers” class to return an instance of the “OddNumberEnumerator” class.
After doing the above, we can still use foreach loop to iterate upon an instance of our “OddNumbers” class.
What’s important to understand is that the “yield” keyword creates the enumerator for us and its almost always better to use yield unless we need to add custom logic for the iteration process.
Can also iterate using the enumerator
One final thing before we sign off. In both scenarios (either using yield or manually creating a custom enumerator), we can use the enumerator to iterate upon an instance of our class. We don’t have to use foreach loop.
Again, it's always better to use foreach loop since it abstracts away the manual use of enumerators and makes our life a whole lot easier.
And that is all about IEnumerable stuff for now.