If you’re newer to C# or programming in general, you may have used an iterator and not even realized it. Iterators can be a performant and effective tool that we have access to as .NET developers that allow us to traverse collections of data. Because one of the requirements of an iterator is that it must implement the IEnumerable interface, the results of an iterator can only be enumerated over. For example, you could use the results of an iterator in a foreach loop but you could not directly index into the iterator results (like you could an array) without some additional steps. Another requirement of iterators is that they use a special keyword called “yield” so that they can yield and return the individual elements that are to be provided to the caller of the iterator.
In a nutshell, iterators allow you to write code that will feed one item at a time to a caller that is enumerating an IEnumerable one element at a time. But what better way to learn about this than with some simple code examples?
By the way, these are all available on GitHub if you want to visit this link and try it out yourself!
Let’s have a look at the following block of code:
int[] dataset = new int[] { 1, 2, 3, 4, 5 };
// function that returns the collection directly
IEnumerable
As we can see, we declare an array with 5 elements inside of it. The first function that is called SimpleENumerable
directly returns the array. As such, it is meeting only one of the two requirements we mentioned for an iterator (that it has an IEnumerable return type, but it does not yield return) and therefore is just a normal method.
Conversely, when we look at the second function called SimpleIterator
, we can see that it does in fact meet both requirements. It’s also important to note that we are able to yield and return individual items because we have a foreach loop iterating over the individual items of the dataset
array. So to say a different way, we are yielding back each individual element of the dataset
array to the caller which will be able to enumerate the results of our iterator one at a time.
The output of running this will not be very surprising as it will appear just like it would if we iterated over the array itself directly. The output is as follows:
Foreach in simple iterator...
1
2
3
4
5
Iterators afford us the ability to inject additional logic during the iteration process. We’ll start with a simple example just to illustrate the execution order where you could theoretically hook into for your iterators:
IEnumerable
The code above shows an iterator just like from the first example, but we’ve added additional writing to the console. We take note of the entering of the iterator, before and after the yield return line, and finally at the end of the iterator. While this example is contrived (i.e. you likely don’t need to write iterators that are writing aggressively to the console) when we look at the resulting output we should be able to understand the behavior this gives us access to:
Foreach in console writing iterator...
Printing to console at start of iterator!
Printing to console before yield return!
1
Printing to console after yield return!
Printing to console before yield return!
2
Printing to console after yield return!
Printing to console before yield return!
3
Printing to console after yield return!
Printing to console before yield return!
4
Printing to console after yield return!
Printing to console before yield return!
5
Printing to console after yield return!
Printing to console at end of iterator!
We can see that in the output we get the single start and end lines when entering and exiting the iterator itself. We also get the before/after log lines printed wrapped around the numeric value that is printed to the console from the foreach loop.
With this in mind, one could consider different opportunities and reasons for why we may want to write additional logic inside of an iterator. The following are some examples to get you thinking, but I am not necessarily suggesting you do or do not write code that does these:
There are plenty of examples to consider here where we’d otherwise be unable to do this quite as easily with a collection.
And no, I don’t mean that your iterators are kicking back on the beach drinking pina coladas while you write all this complex code. I mean that they are evaluated lazily so that they are technically only executed when they are enumerated.
Let’s demonstrate this by looking at the following code example:
IEnumerable
The comments in the code are a bit of a spoiler, but let’s read through to see what we have. First, the method at the top is *not* an iterator because it does not yield anything. We can see that it asks to sleep for 5 seconds before returning. The second method is in face an iterator, and it’s written to be as similar as possible to the method above it except that it will yield return instead of returning the underlying collection directly.
The code that exercises these two methods is surrounded by printing to the console with some time stamps. It’s also important to note that we’re not even calling a foreach loop or otherwise “materializing” the results of the enumerator, we’re simply assigning to the variables on the left hand side.
And the result?
The non-iterator pays the performance hit but the iterator assignment is essentially instantaneous. That’s what we mean by lazy.
So if you’re keen you might be asking: “Well it must not have actually enumerated anything because it would need to sleep 5 seconds before yielding back even the first item!”
And you’d be 100% correct. The iterator did not pay any performance impact here because we did not in fact enumerate it. The assignment of the iterator effectively acts like setting a function pointer. It will only pay the performance cost of the iterator when you iterate.
In this article we looked at a high level overview of iterators when contrasted with a simple collection like an array. If this was still a bit fuzzy, you may find a benefit from reading my prior article on IEnumerable. After looking through some examples, we can see that iterators allow us to have additional logic before we decide to yield back individual items to a caller. In our last example, we also saw that iterators have a lazy characteristic where just assigning them seems to be essentially free but we will be paying the cost to iterate them later.
If the lazy evaluation has got the gears turning for you, then you’re well on your way to understanding some of the more advanced topics. From here, I would suggest reading about some of the pitfalls for C# code that is designed around passing around materialized collections compared with some of the pitfalls that are experienced when codebases heavily use iterators with developers that don’t understand their characteristics.
Level up your software engineering abilities and sign up to my email list below!
In C# and .NET, as programmers we have access to an interface that is called IEnumerable (or IEnumerable<T> for the generic version). Using IEnumerable allows us to iterate from a collection or data source by moving one element at a time. It’s also important to note that all collection types in C# inherit from IEnumerable so collections you are familiar with like arrays and lists implement IEnumerable. I have been trying to help educate around IEnumerable usage for many years now so this is a...
If you’re new to programming then without a doubt you’ve had to ask yourself what is the best beginner programming language so that you know where to focus your efforts. You’re about to go invest all of this time and mental effort into learning a new skill, so of course you want to make sure you’re starting on the right track. I would be willing to bet you that I know what the number one language you’re told to start with is after you do a bit of searching on the Internet. I’m here to tell...