現象
こういうクラスを作った。
class Foo
{
int[] _array;
int _pos;
public Foo(int[] array) => _array = array;
public IEnumerable<int> Yield()
{
while (_pos < _array.Length)
yield return _array[_pos++];
}
}
中身を出力してみた。
static void Main(string[] args)
{
var foo = new Foo(new int[] { 0, 1, 2, 3, 4, 5 });
var a = foo.Yield();
if (!a.Any())
return;
// "012345"が出力されてほしいのに
foreach (var x in a)
Console.WriteLine(x);
// ⇒ "12345"
}
0
が消失した。
原因
if (!a.Any())
を呼んだ時点で_pos
がインクリメントされてしまっているため、そのあとのforeach
では0
が出力されなくなってしまう。
ちなみに、以下のようにするとAny()
をしても1回目のyield
の時点で止まるので_pos
は変わらず、foreachで
0`が出力されるようになる。
class Foo
{
int[] _array;
int _pos;
public Foo(int[] array) => _array = array;
public IEnumerable<int> Yield()
{
while (_pos < _array.Length)
{
yield return _array[_pos];
// インクリメントを分ける
_pos++;
}
}
}
これで元のコードは動くようになるものの、コメントをいただいた例のように、if (!a.Any())
をif (!a.Any(n => n == 5))
に置き換えると、再び思った出力が得られなくなる(今度は5
だけが出力される)。
Foo.Yield()
を呼ぶたびに_pos
がリセットされるようになっていないのが根本的な原因。
考察
yield return
を記述したメソッドは、コンパイラが生成したステートマシンに処理が移譲されるが、このステートマシンはIEnumerable<T>
とIEnumerator<T>
の両方を実装している。
ということは、このステートマシンが正しくIEnumerator<T>
としての役割を果たすためには、yield return
を記述したメソッド自身が呼び出された際のコンテキストを保存する(自らの数え上げを何度でも再現できる)ようになっている必要がある。
ところが今回、Foo.Yield()
はFoo._pos
を数え上げに使用してしてしまっているのでその条件を満たしていない。つまりIEnumerator<T>
としての役割を果たしていないので予想していない挙動になった。
ということでしょうか...?