Any
will enumerate the IEnumerable<T>
.
In this case, GetNums
just so happens to return an IEnumerable<int>
object, whose GetEnumerator()
method returns a new enumerator, that is reset back to the initial state.
Here are some excerpts of the decompiled code that https://sharplab.io/ generates, which makes it very clear what's going on.
// GetNums returns an instance of type <GetNums>d__0, with a state of -2
internal static IEnumerable<int> GetEnums()
{
return new <GetNums>d__0(-2);
}
// After Any() consumes the first element of the enumerator, the state is no longer -2
// so GetEnumerator in <GetNums>d__0 returns 'new <GetNums>d__0(0)', which has a state of 0.
// this is as if GetEnums has not been executed at all
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
{
<>1__state = 0;
return this;
}
return new <GetNums>d__0(0);
}
Reading the spec, I can't see where this behaviour is specified, so another implementation may very well choose not to have this behaviour.
If GetNums
were not a pure function, and it yield return SomeSideEffect()
instead, SomeSideEffect()
will be called twice, and might end up yielding a different value the second time.
To show that Any()
does call MoveNext
on the enumerator, you can write your own type that implements IEnumerable<T>
and IEnumerator<T>
, and in the GetEnumerator
method, return this
(unlike what the compiler generates for an iterator block).
class OneToFive: IEnumerable<int>, IEnumerator<int> {
public IEnumerator<int> GetEnumerator() => this;
IEnumerator IEnumerable.GetEnumerator() => this;
public int Current { get; set; }
object IEnumerator.Current => Current;
public bool MoveNext() {
if (Current > 4) return false;
Current++;
return true;
}
public void Reset() { Current = 0; }
public void Dispose() {}
}
var nums = new OneToFive();
Console.WriteLine(nums.Any()); // True
Console.WriteLine(string.Join(", ", nums)); // 2, 3, 4, 5