0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

状態を持つクラスのメソッドでyield returnした時にはまった現象

Last updated at Posted at 2021-09-26

現象

こういうクラスを作った。

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は変わらず、foreach0`が出力されるようになる。

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>としての役割を果たしていないので予想していない挙動になった。
ということでしょうか...?

0
1
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?