0

Below is the custom class I made:

 class B : IEnumerable
    {
        int[] data = { 0, 1, 2, 3, 4 };
        public IEnumerator GetEnumerator()
        {
            Console.WriteLine("Called");
            return new BEnumerator(this);
        }

        private class BEnumerator : IEnumerator
        {
            private B instance;
            private int position = -1;

            public BEnumerator(B inst)
            {
                this.instance = inst;
            }

            public object Current
            {
                get
                {
                    return instance.data[position];
                }
            }

            public bool MoveNext()
            {
                position++;
                return (position < instance.data.Length);
            }

            public void Reset()
            {
                position = -1;
            }
        }
    }

if we iterates through foreach:

 B b = new B();

 foreach (var item in b)
 {
    Console.WriteLine(item);
 }

 foreach (var item in b)
 {
    Console.WriteLine(item);
 }

and the ouput is

called
0
1
2
3
4
called
0
1
2
3
4

we can see that GetEnumerator() was called twice since we use two foreach loop, each foreach call GetEnumerator() once, fair enough.

But if we modified the iterator as

 public IEnumerator GetEnumerator()
 {
     yield return data[0];
     yield return data[1];
     yield return data[2];
     yield return data[3];
     yield return data[4];
 }

it can easily be seen that GetEnumerator() will be called five times to get each value. So how come GetEnumerator() is called only once and sometimes multiple times, which is inconsistent?

P.S I know the fact that if you run the code with yield, the result is the same and GetEnumerator() seems to be called twice, but because yield is special, which makes the whole method seems to be only called once for every foreach, but the method has to be called multiple times in the background (GetEnumerator() will be called 10 times in this case)

4
  • FWIW If I add the requisite Console.WriteLine("Called"); to the modified iterator, I get the same result as the first. Commented Jun 6, 2019 at 5:00
  • @John that's true, but because yield is special, which makes the whole method seems to be only called once( the final results are the same), but the method has to be called multiple times in the background Commented Jun 6, 2019 at 5:02
  • Judging by the decompiled code in TheGeneral's answer, it seems that it is only called once. It's just that the compiler generates the enumerator class for you. It seems little different to your own BEnumerator. Commented Jun 6, 2019 at 5:07
  • "the method has to be called multiple times in the background" -- no, it doesn't. The GetEnumerator() method is called exactly once. The compiler rewrites what looks like a single method into a whole new class, with an implementation of IEnumerator.MoveNext() that winds up with a state machine and most of your method's actual code. See marked duplicate for an extensive discussion on how this works. Commented Jun 6, 2019 at 5:22

1 Answer 1

2

Put quite simply (and disregarding the fact that GetEnumerator is called the same amount of times), yield is a special case...

yield (C# Reference)

Excerpt from the example given in the documentation

On an iteration of the foreach loop, the MoveNext method is called for elements. This call executes the body of MyIteratorMethod until the next yield return statement is reached. The expression returned by the yield return statement determines not only the value of the element variable for consumption by the loop body but also the Current property of elements

The compiler generates code for the method and plumbs up a class that implements IEnumerator to do the enumeration (like the one you have), as you can see here

Note : There are oodles of places the compiler generates code for you and does special things you can't do (which you might call inconsistent)

Given this

public IEnumerator GetEnumerator()
{
    yield return data[0];
    yield return data[1];
    yield return data[2];
    yield return data[3];
    yield return data[4];
}

The compiler generates your method like this

[IteratorStateMachine(typeof(<GetEnumerator>d__1))]
public IEnumerator GetEnumerator()
{
    <GetEnumerator>d__1 <GetEnumerator>d__ = new <GetEnumerator>d__1(0);
    <GetEnumerator>d__.<>4__this = this;
    return <GetEnumerator>d__;
}

And Generates a class like this

[CompilerGenerated]
private sealed class <GetEnumerator>d__1 : IEnumerator<object>, IDisposable, IEnumerator
{
  private int <>1__state;

  private object <>2__current;

  public C <>4__this;

  object IEnumerator<object>.Current
  {
      [DebuggerHidden]
      get
      {
          return <>2__current;
      }
  }

  object IEnumerator.Current
  {
      [DebuggerHidden]
      get
      {
          return <>2__current;
      }
  }

  [DebuggerHidden]
  public <GetEnumerator>d__1(int <>1__state)
  {
      this.<>1__state = <>1__state;
  }

  [DebuggerHidden]
  void IDisposable.Dispose()
  {
  }

  private bool MoveNext()
  {
      switch (<>1__state)
      {
          default:
              return false;
          case 0:
              <>1__state = -1;
              <>2__current = <>4__this.data[0];
              <>1__state = 1;
              return true;
          case 1:
              <>1__state = -1;
              <>2__current = <>4__this.data[1];
              <>1__state = 2;
              return true;
          case 2:
              <>1__state = -1;
              <>2__current = <>4__this.data[2];
              <>1__state = 3;
              return true;
          case 3:
              <>1__state = -1;
              <>2__current = <>4__this.data[3];
              <>1__state = 4;
              return true;
          case 4:
              <>1__state = -1;
              <>2__current = <>4__this.data[4];
              <>1__state = 5;
              return true;
          case 5:
              <>1__state = -1;
              return false;
      }
  }

  bool IEnumerator.MoveNext()
  {
      //ILSpy generated this explicit interface implementation from .override directive in MoveNext
      return this.MoveNext();
  }

  [DebuggerHidden]
  void IEnumerator.Reset()
  {
      throw new NotSupportedException();
  }
}
Sign up to request clarification or add additional context in comments.

3 Comments

could you explain the generated code a little more, I couldn't understand the code. or you can tell me is GetEnumerator() called 10 times with yield?
@slowjams The class <GetEnumerator>d__1 is equivalent to your BEnumerator class. GetEnumerator() is equivalent to the one where you return BEnumerator, and it's called once. The only thing that muddies it is the names of the elements involved. But that's all they are: names.
@slowjams disregaurd all the <> and _ its just the Iterator design pattern its nearly the same as your class