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)

  • FWIW If I add the requisite `Console.WriteLine("Called");` to the modified iterator, I get the same result as the first. – ProgrammingLlama Jun 06 '19 at 05: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 –  Jun 06 '19 at 05: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`. – ProgrammingLlama Jun 06 '19 at 05: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. – Peter Duniho Jun 06 '19 at 05:22

1 Answers1

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();
  }
}
halfer
  • 19,824
  • 17
  • 99
  • 186
TheGeneral
  • 79,002
  • 9
  • 103
  • 141
  • 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? –  Jun 06 '19 at 05:09
  • @slowjams The class `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. – ProgrammingLlama Jun 06 '19 at 05:12
  • @slowjams disregaurd all the `<>` and `_` its just the *Iterator design pattern* its nearly the same as your class – TheGeneral Jun 06 '19 at 05:13