Skip to content

Iterators C# 2.0correctness

Produce and consume sequences.

The foreach keyword allows sequences and collections to be looped over without worrying about off-by-1 bugs. It achieves this by way of the IEnumerable and IEnumerator interfaces and their generic counterparts.

Producing these sequences required an additional class to act as the iterator keeping track of the location within the sequence so that two different foreach operations upon the same source do not interfere with each other.

C# 2.0 brings two new operations in order to simplify producing these enumerable sequences. The compiler does this by generating a state machine that returns control to the consumer but maintains the state of the method emitting the sequence so it can be called again when the next item in the sequence is requested.

Each operation returns control to the consumer:

  • yield return x - also supplies x as the next item in the sequence
  • yield break - indicates the sequence is now complete and there are no more items

Code ​

csharp
class RangeEnumerable : IEnumerable<int>
{
    int lower, upper;

    public RangeEnumerable(int lower, int upper)
    {
        this.lower = lower;
        this.upper = upper;
    }

    public IEnumerator<int> GetEnumerator()
    {
        int current = lower;
        while (current <= upper)
            yield return current++;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}
csharp
class RangeEnumerable : IEnumerable<int>
{
    int lower, upper;

    public RangeEnumerable(int lower, int upper)
    {
        this.lower = lower;
        this.upper = upper;
    }

    public IEnumerator<int> GetEnumerator()
    {
        return new RangeEnumerator(lower, upper);
    }

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

class RangeEnumerator : IEnumerator<int>
{
    int lower, upper, current;
    bool started;

    public RangeEnumerator(int lower, int upper)
    {
        this.lower = lower;
        this.upper = upper;
        current = lower;
    }

    public int Current { get { return current; } }

    public bool MoveNext()
    {
        if (!started)
            started = true;
        else
            current++;
        return current <= upper;
    }

    public void Reset()
    {
        current = lower;
        started = false;
    }

    // Dispose and non-generic IEnumerator.Current omitted
}

Notes ​

  • A method or property getter can be an iterator too, e.g. IEnumerable<int> Range(int lower, int upper) with yield return inside, removing the need for a class at all
  • Iterators are lazy: none of the body runs until the consumer calls MoveNext, and it only runs as far as the next yield return
  • Compiler-generated iterators do not support Reset and throw NotSupportedException if it is called

More information ​

Powered by Intracia