1

How could I rewrite GetEnumerator method without using yield keyword? Code of method:

public IEnumerator<int> GetEnumerator()
{
    yield return 1;

    Console.WriteLine("1");

    yield return 2;
}

I just what to know how it can be implemented manually.

Ondrej Tucny
  • 27,626
  • 6
  • 70
  • 90
Jorge
  • 89
  • 9
  • 1
    Possible duplicate of [How do I implement IEnumerable](https://stackoverflow.com/questions/11296810/how-do-i-implement-ienumerablet) – CodeNotFound May 12 '18 at 08:52
  • The IL generated from any IEnumerator is always generating a yield statement as far as I know. I'd check this if I was you, you could be trying to generate some performance benefit descoping local variables for GC collection that the IL just pulls back in. – Walter Verhoeven May 12 '18 at 09:10
  • @CodeNotFound The OP's question is *not* how to implement `IEnumerable`, but how to implement `IEnumerator` manually. – Ondrej Tucny May 12 '18 at 09:21

1 Answers1

8

In fact, the yield statement is a syntactic sugar that makes the compiler actually generate a class which implements the IEnumerator<T> interface and rewrites the body of the method using the yield statement into a state machine.

Each state is associated with a portion of code that ultimately generates the next element in the sequence. This is embedded in the MoveNext() method. A state machine can represent all necessary constructs (a sequence, a selection, an iteration) and hence all C# code (meaning statements within a method) can be rewritten like that. That is the underlying 'magic' of yield.

In your specific case, such rewriting into a state machine and the corresponding full implementation of IEnumerator<T> (and its inherited IEnumerator (non-generic) and IDisposable interfaces) will look like this:

public class CustomEnumerator : IEnumerator<int>
{
    public int Current { get; private set; }

    object IEnumerator.Current => this.Current;

    // internal 'position' in the sequence, i.e. the current state of the state machine
    private int position = 0;

    public bool MoveNext()
    {
        // advance to next state 
        // (works for linear algorithms; an alternative is to select the next state at the end of processing the current state)
        position++;

        // perform the code associated with the current state and produce an element
        switch (position)
        {
            // state 1: line 'yield return 1;'
            case 1:
                Current = 1;
                return true;

            // state 2: lines 'Console.WriteLine("1");' and 'yield return 2;'
            case 2:
                Console.WriteLine("1"); // see also note at the end of this answer
                Current = 2;
                return true;

            // there are no other states in this state machine
            default:
                return false;
        }
    }

    public void Reset()
    {
        position = 0;
    }

    public void Dispose()
    {
        // nothing to do here   
    }
}

Each call to MoveNext(), which is what happens internally in each iteration of a foreach statement, causes a portion of the code to be executed—until the next element in the sequence is produced.

For this implementation to be usable, a corresponding IEnumerable<T> implementation is necessary, which is pretty trivial:

public class CustomEnumerable : IEnumerable<int>
{
    public IEnumerator<int> GetEnumerator()
    {
        return new CustomEnumerator();
    }

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

Then the following two foreach loops will produce the exact same results:

void Main()
{
    // custom implementation of IEnumerator<T>
    foreach (int i in new CustomEnumerable())
    {
        Console.WriteLine(i);
    }   

    // your original implementation—will produce same results
    // note: I assume someObject implements IEnumerable<T> and hence your GetEnumerator() method
    foreach (int i in someObject)
    {
        Console.WriteLine(i);
    }   
}

Note: In your GetEnumerator() code the call to Console.WriteLine("1"); is after the enumerator returns 1 (and the caller processes it) so that looks a little bit odd.

Ondrej Tucny
  • 27,626
  • 6
  • 70
  • 90