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 sequenceyield break- indicates the sequence is now complete and there are no more items
Code
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();
}
}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)withyield returninside, 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 nextyield return - Compiler-generated iterators do not support
Resetand throwNotSupportedExceptionif it is called