Iterators aren’t magic. We’ve seen the IL2CPP output for them and it’s not complex. It turns out we can just as easily implement our own iterators and gain some nice advantages along the way. Read on to learn how!

Syntactic sugar

Iterators allow us programmers to use a terse syntax to achieve something we otherwise could have with a lot more typing. They’re essentially a shortcut for creating a class that implements IEnumerator, IEnumerator<T>, IEnumerable, or IEnumerable<T> by typing what appears to be a regular function that happens to use yield return, yield break, or both.

Knowing this, we can simply type out the longer version of an iterator by making the class ourselves. One particular advantage of this approach is that we can opt to use a struct instead of a class. The resulting type is then usable within Unity’s C# jobs system when the equivalent class-based iterator would not.

Starting simple

Let’s start out with a simple iterator function that might be used as a coroutine on a MonoBehaviour:

IEnumerator PrintUrlIterator(string url)
{
    WWW www = new WWW(url);
    while (!www.isDone)
    {
        yield return null;
    }
    print(www.text);
}

All this does is download a URL and print its body as text. For example, it could be used to print out the HTML of a web page:

StartCoroutine(PrintUrlIterator("https://example.com"));

Now let’s go ahead and manually implement a version of this iterator. We’ll step through one bit at a time:

public struct PrintUrl : IEnumerator
{

This establishes the type as a struct that implements IEnumerator, analagous to the compiler-generated class that implements IEnumerator we get when using iterator functions.

private string url;
private WWW www;

Local variables are simply converted to fields, just as the compiler does with iterators.

public PrintUrl(string url)
{
    this.url = url;
    www = null;
}

We use a constructor to save parameters to fields, but don’t actually start the download. This matches what happens when calling an iterator like PrintUrlIterator("https://example.com").

public bool MoveNext()
{
    if (www == null)
    {
        www = new WWW(url);
    }
 
    if (!www.isDone)
    {
        return true;
    }
 
    print(www.text);
    return false;
}

The MoveNext function performs all the work of the iterator. The first if block handles the initial case where the iterator hasn’t started by executing the first line of the iterator.

The second if block replaces the while loop that waits until the download is done. Returning true indicates that there’s more work to be done.

Finally, the print is executed to perform the work at the end of the iterator. Returning false indicates that the iterator is now done.

The rest of the struct is full of boilerplate:

public object Current
{
    get
    {
        return null;
    }
}
 
public void Reset()
{
    throw new NotSupportedException();
}

The Current property returns the current value of the iterator. This always returns null because the iterator always uses yield return null. The more terse public object Current => null; can be used instead if C# 6 is enabled via .NET 4.x support available on newer versions of Unity.

The Reset function can’t be used with iterators, so we duplicate its exception-throwing behavior here. This is one area where we enjoy increased flexibility by manually implementing the iterator. If we wanted to, we could allow for resetting the iterator quite simply by instead setting www to null just as in the constructor.

Using this type is just as easy as using the iterator function it replaces:

StartCoroutine(new PrintUrl("https://example.com"));

Keep in mind that StartCoroutine takes an IEnumerator and the PrintUrl struct will therefore be boxed at this point and garbage-collected sometime after the coroutine finishes.

State machines

The above PrintUrl example is implemented in a way that takes advantage of how the iterator works. This is contrary to the general approach that the compiler takes when implementing our iterator classes. That doesn’t mean we can’t take the same approach as the compiler when we want to implement an iterator in a more structured way.

Let’s start again with an example iterator:

IEnumerator DoFourThingsIterator()
{
    print("Did A");
    yield return null;
    print("Did B");
    yield return null;
    print("Did C");
    yield return null;
    print("Did D");
}

We can use this in the same way:

StartCoroutine(DoFourThingsIterator());

Now let’s convert this to a struct type:

public struct DoFourThings : IEnumerator
{
    private enum State
    {
        ThingA,
        ThingB,
        ThingC,
        ThingD,
        Done
    }
 
    private State state;

Just as the compiler does, we’ve created a little state machine. For clarity, we have an enum with the five possible states that our iterator can be in. Since all fields of a struct are initialized to zero and the first enumerator of an enum is zero by default, we start out in the ThingA state.

public bool MoveNext()
{
    switch (state)
    {
        case State.ThingA:
            print("Did A");
            state = State.ThingB;
            return true;
        case State.ThingB:
            print("Did B");
            state = State.ThingC;
            return true;
        case State.ThingC:
            print("Did C");
            state = State.ThingD;
            return true;
        case State.ThingD:
            print("Did D");
            state = State.Done;
            return false;
        case State.Done:
        default:
            return false;
    }
}

MoveNext, which implements the iterator’s functionality, simply uses a switch to execute the current state’s work. Each state’s case then switches to the next state and returns true to indicate that more work is yet to come. In the final state (ThingD), we switch to the Done state which does no work.

The same boilerplate completes the struct:

public object Current
{
    get
    {
        return null;
    }
}
 
public void Reset()
{
    throw new NotSupportedException();
}

We can again use this type in the same way as the iterator it replaces:

StartCoroutine(new DoFourThings());
Returning values

So far we’ve always used yield return null, but some iterators yield more meaningful values. Let’s take a look at one that enumerates a sequence:

IEnumerable<int> RangeIterator(int from, int to, int step)
{
    for (int i = from; i <= to; i += step)
    {
        yield return i;
    }
}

Note that this is not only returning a value but is also an IEnumerable rather than just an IEnumerator. This requests that the compiler generate not only an enumerator but an enumerable that creates that enumerator type. The advantage is that we can now use it with a foreach loop:

foreach (int i in RangeIterator(0, 30, 10))
{
    print(i);
}

This prints:

0
10
20
30

Now let’s do the conversion, starting with the enumerator type:

public struct RangeEnumerator : IEnumerator<int>
{
    private readonly int to;
    private readonly int step;
    private int i;
 
    public RangeEnumerator(int from, int to, int step)
    {
        this.to = to;
        this.step = step;
        i = from - step;
    }

Just like before, we store the necessary local variables of the iterator as fields of the struct. Since MoveNext will be called before Current, we need to offset it to make sure that the initial call moves it to the initial value (from).

public bool MoveNext()
{
    i += step;
    return i <= to;
}

This MoveNext is exceedingly simple since it just replaces the second and third terms of the for loop in the iterator function.

public int Current
{
    get
    {
        return i;
    }
}
 
object IEnumerator.Current
{
    get
    {
        return Current;
    }
}

We need two Current properties this time: one for IEnumerator<T> and one for IEnumerator. We can implement the latter in terms of the former to reduce code duplication, even though this particular property happens to be trivial.

public void Reset()
{
    throw new NotSupportedException();
}
 
public void Dispose()
{
}

The Reset remains the same, but we now have an additional Dispose to fill out because IEnumerator<T> requires it. There’s nothing to dispose here, so we’ll simply leave it empty.

Now let’s implement the enumerable type that returns this type of enumerator:

public struct Range : IEnumerable<int>
{

This time we’re making an IEnumerable, not an IEnumerator, and we’re still making a struct. Here are the fields:

private readonly int from;
private readonly int to;
private readonly int step;
 
public Range(int from, int to, int step)
{
    this.from = from;
    this.to = to;
    this.step = step;
}

The enumerable type just exists to create the enumerator type, so all we do here is save all the data necessary to pass to it. No logic at all needs to go in here.

public RangeEnumerator GetEnumerator()
{
    return new RangeEnumerator(from, to, step);
}
 
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
    return GetEnumerator();
}
 
IEnumerator IEnumerable.GetEnumerator()
{
    return GetEnumerator();
}

We have three GetEnumerator functions here: one returning RangeEnumerator, one for IEnumerable<T>, one for IEnumerable. The one returning RangeEnumerator is the most concrete of the three while the other two are there to satisfy the requirements of the IEnumerable and IEnumerable<T> interfaces. The compiler will use the appropriate overload depending on the type of variable that GetEnumerator or foreach is operating on.

The interface compliance overloads are implemented by just calling the overload that returns a RangeEnumerator to avoid code duplication. In this case, all that method does is create an enumerator using the fields we initialized in the constructor, but it’s still good to reduce even this small amount of duplication.

This actually completes the Range type, so let’s move on to using it:

foreach (int i in new Range(0, 30, 10))
{
    print(i);
}

Once again we have nearly identical usage code. There’s one difference though: in this case there’s no call to StartCoroutine. That means there’s no boxing to IEnumerator anymore. Just as with foreach loops over other struct-based enumerator types like the ones returned by List<T> and Dictionary<TKey, TValue>, we won’t suffer any GC impacts with this code because the compiler knows to call the GetEnumerator overload that returns a RangeEnumerator. The same can’t be said about the iterator function, which is based on a compiler-generated class.

Just as with the iterator function, this prints:

0
10
20
30
Jobs integration

Now let’s realize one final advantage of this approach by integrating one of the converted iterators into a job:

struct SumJob : IJob
{
    public Range Range;
    public NativeArray<int> Sum;
 
    public void Execute()
    {
        foreach (int i in Range)
        {
            Sum[0] += i;
        }
    }
}

Because Range is a struct and not a class, we can store it as an input field of job types like SumJob. The Execute function can then use foreach to iterate over it just like we did outside of the jobs system.

Here’s how we can use this job:

NativeArray<int> sum = new NativeArray<int>(1, Allocator.Temp);
SumJob job = new SumJob { Range = new Range(0, 30, 10), Sum = sum };
job.Run();
print(sum[0]);
sum.Dispose();

Here we very simply pass a Range parameter to the job by setting it as a field. This code prints 60, the correct sum of 0 + 10 + 20 + 30.

Conclusion

Iterator syntax provides a concise way to create an IEnumerator, IEnumerator<T>, IEnumerable, or IEnumerable<T>, but writing our own types provides some advantages. First, we can implement these types as structs to avoid garbage creation and the wrath of the GC. Second, we can use these types as parameters to C# jobs. Third, we can implement Reset to allow for reusing enumerator types.

These advantages are offset by the extra typing that’s required and the resulting less-readable code. Whether that tradeoff is one you’d like to make is up to you and probably varies on a case-by-case basis. Think of these as another tool in your toolbox. It may come in handy when you’re in need of one of its advantages.