サーチ…


前書き

ステートメントでyieldキーワードを使用する場合は、それが現れるメソッド、演算子、またはアクセサーをイテレータとして指定します。 yieldを使用してイテレータを定義すると、カスタムコレクション型のIEnumerableおよびIEnumeratorパターンを実装するときに、明示的な余分なクラス(列挙の状態を保持するクラス)が不要になります。

構文

  • yield return [TYPE]
  • 収穫逓減

備考

戻り値の型がIEnumerableIEnumerable<T>IEnumerator 、またはIEnumerator<T>メソッドにyieldキーワードを設定すると、戻り値の型( IEnumerableまたはIEnumerator )の実装が生成されます。それぞれの結果を得るためにそれぞれの "収量"までの

yieldキーワードは、論理的に無制限のシーケンスの「次の」要素を返す場合に便利です。事前にシーケンス全体を計算することは不可能です。また、返される前に完全なシーケンスを計算すると、ユーザーにとって望ましくない一時停止につながります。

yield breakを使用して、いつでも配列を終了することができます。

yieldキーワードはIEnumerable<T>ような戻り型としてイテレータのインターフェイス型を必要とするため、これをTask<IEnumerable<T>>オブジェクトを返すので非同期メソッドでは使用できません。

参考文献

簡単な使用法

yieldキーワードは、 IEnumerableまたはIEnumeratorIEnumerator派生した汎用バリアント)を返す関数を定義するために使用されます。この値は、呼び出し元が返されたコレクションを反復処理するときに遅延的に生成されます。 備考欄に目的の詳細をお読みください。

次の例は、 forループの中にあるyield returnステートメントを持っています。

public static IEnumerable<int> Count(int start, int count)
{
    for (int i = 0; i <= count; i++)
    {
        yield return start + i;
    }
}

次に、それを呼び出すことができます:

foreach (int value in Count(start: 4, count: 10))
{
    Console.WriteLine(value);
}

コンソール出力

4
5
6
...
14

.NET Fiddleのライブデモ

foreachステートメント本体の各反復では、反復回数Count関数が呼び出されます。イテレーター関数の各呼び出しは、次回のforループの繰り返し中に発生するyield returnステートメントの次の実行に進みます。

より適切な使用法

public IEnumerable<User> SelectUsers()
{
    // Execute an SQL query on a database.
    using (IDataReader reader = this.Database.ExecuteReader(CommandType.Text, "SELECT Id, Name FROM Users"))
    {
        while (reader.Read())
        {
            int id = reader.GetInt32(0);
            string name = reader.GetString(1);
            yield return new User(id, name);
        }
    }
}

もちろん、SQLデータベースからIEnumerable<User>を取得する他の方法もあります。これはyieldを使用して "要素のシーケンス"セマンティクスを持つものを誰かが繰り返し処理できるIEnumerable<T>変換できることを示しています。

早期終了

既存のyieldメソッドの機能を拡張するには、内ループの実行を停止するyield breakyield breakを呼び出して、関数内で終了条件を定義できる1つ以上の値または要素を渡します。

public static IEnumerable<int> CountUntilAny(int start, HashSet<int> earlyTerminationSet)
{
    int curr = start;

    while (true)
    {
        if (earlyTerminationSet.Contains(curr))
        {
            // we've hit one of the ending values
            yield break;
        }

        yield return curr;

        if (curr == Int32.MaxValue)
        {
            // don't overflow if we get all the way to the end; just stop
            yield break;
        }

        curr++;
    }
}

上記のメソッドは、 earlyTerminationSet内の値の1つが検出されるまで、指定されたstart位置から反復します。

// Iterate from a starting point until you encounter any elements defined as 
// terminating elements
var terminatingElements = new HashSet<int>{ 7, 9, 11 };
// This will iterate from 1 until one of the terminating elements is encountered (7)
foreach(var x in CountUntilAny(1,terminatingElements))
{
    // This will write out the results from 1 until 7 (which will trigger terminating)
    Console.WriteLine(x);
}

出力:

1
2
3
4
5
6

.NET Fiddleのライブデモ

引数を正しくチェックする

イテレータメソッドは、戻り値が列挙されるまで実行されません。したがって、イテレータの外部で前提条件を宣言することは有益です。

public static IEnumerable<int> Count(int start, int count)
{
    // The exception will throw when the method is called, not when the result is iterated
    if (count < 0)
        throw new ArgumentOutOfRangeException(nameof(count));

    return CountCore(start, count);
}

private static IEnumerable<int> CountCore(int start, int count)
{
    // If the exception was thrown here it would be raised during the first MoveNext()
    // call on the IEnumerator, potentially at a point in the code far away from where
    // an incorrect value was passed.
    for (int i = 0; i < count; i++)
    {
        yield return start + i;
    }
}

コールサイドコード(使用法):

// Get the count
var count = Count(1,10);
// Iterate the results
foreach(var x in count)
{
    Console.WriteLine(x);
}

出力:

1
2
3
4
5
6
7
8
9
10

.NET Fiddleのライブデモ

メソッドがyieldを使用して列挙型を生成すると、コンパイラは反復処理時にコードをyieldまで実行するステートマシンを作成します。次に、降伏したアイテムを返し、その状態を保存します。

これは、最初の要素を試してアクセスしたときにのみ、メソッドを最初に呼び出すときに無効な引数( nullを渡すなど)が見つからないことを意味します。メソッドは状態マシンによって実行されます)。引数を最初にチェックする通常のメソッドでラップすることによって、メソッドが呼び出されたときに引数をチェックすることができます。これは失敗の一例です。

C#7 CountCoreを使用する場合、 CountCore関数は、 ローカル関数として便利にCount関数に隠すことができます。 ここの例を参照してください。

Enumerableを返すメソッド内の別のEnumerableを返す

public IEnumerable<int> F1()
{
    for (int i = 0; i < 3; i++)
        yield return i;

    //return F2(); // Compile Error!!
    foreach (var element in F2())
        yield return element;
}

public int[] F2()
{
    return new[] { 3, 4, 5 };
}

レイジー評価

foreach文が次の項目に移動するときのみ、イテレータブロックは次のyield文まで評価されます。

次の例を考えてみましょう。

private IEnumerable<int> Integers()
{
    var i = 0;
    while(true)
    {
        Console.WriteLine("Inside iterator: " + i);
        yield return i;
        i++;
    }
}

private void PrintNumbers()
{
    var numbers = Integers().Take(3);
    Console.WriteLine("Starting iteration");

    foreach(var number in numbers)
    {
        Console.WriteLine("Inside foreach: " + number);
    }
}

これは出力されます:

反復の開始
イテレータ内:0
内部foreach:0
イテレータ内:1
内部foreach:1
イテレータ内:2
内側foreach:2

デモを見る

結果として:

  • "Starting iteration"は、最初にイテレータメソッドが呼び出されたにもかかわらず印刷されますIntegers().Take(3);これは、行Integers().Take(3);実際に反復を開始しません( IEnumerator.MoveNext()への呼び出しは行われません)
  • コンソールに出力する行は、イテレータメソッド内のものとforeach内のものとの間で交互に表示されます。
  • このプログラムは、iteratorメソッドが途切れないwhile trueを持っていても、 .Take()メソッドのために終了します。

試して...最後に

iteratorメソッドがtry...finally内にyieldを持つ場合、返されたIEnumeratorは、評価の現在の点がtryブロック内にある限り、 Disposeが呼び出されたときfinallyステートメントを実行します。

与えられた関数:

private IEnumerable<int> Numbers()
{
    yield return 1;
    try
    {
        yield return 2;
        yield return 3;
    }
    finally
    {
        Console.WriteLine("Finally executed");
    }
}

電話するとき:

private void DisposeOutsideTry()
{
    var enumerator = Numbers().GetEnumerator();

    enumerator.MoveNext();
    Console.WriteLine(enumerator.Current);
    enumerator.Dispose();
}

次に、それは印刷されます:

1

デモを見る

電話するとき:

private void DisposeInsideTry()
{
    var enumerator = Numbers().GetEnumerator();

    enumerator.MoveNext();
    Console.WriteLine(enumerator.Current);
    enumerator.MoveNext();
    Console.WriteLine(enumerator.Current);
    enumerator.Dispose();
}

次に、それは印刷されます:

1
2
最後に実行された

デモを見る

yieldを使用してIEnumeratorを作成する IEnumerableを実装するとき

IEnumerable<T>インターフェイスは、単一の方法、有するGetEnumerator()を返し、 IEnumerator<T>

yieldキーワードはIEnumerable<T>を直接作成するために使用できますが、まったく同じ方法でIEnumerator<T>を作成することできます。変更されるのは、メソッドの戻り値の型だけです。

これは、 IEnumerable<T>を実装する独自のクラスを作成する場合に便利です。

public class PrintingEnumerable<T> : IEnumerable<T>
{
    private IEnumerable<T> _wrapped;

    public PrintingEnumerable(IEnumerable<T> wrapped)
    {
        _wrapped = wrapped;
    }

    // This method returns an IEnumerator<T>, rather than an IEnumerable<T>
    // But the yield syntax and usage is identical.
    public IEnumerator<T> GetEnumerator()
    {
        foreach(var item in _wrapped)
        {
            Console.WriteLine("Yielding: " + item);
            yield return item;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

(この特定の例は単なる例示であり、 IEnumerable<T>返す単一のイテレータメソッドを使用してより明確に実装できます)。

熱心な評価

yieldキーワードは、コレクションの遅延評価を可能にします。コレクション全体を強制的にメモリにロードすることをeager evaluationと呼びます。

次のコードはこれを示しています:

IEnumerable<int> myMethod()
{
    for(int i=0; i <= 8675309; i++)
    {
        yield return i;
    }
}
...
// define the iterator
var it = myMethod.Take(3);
// force its immediate evaluation
// list will contain 0, 1, 2
var list = it.ToList();

ToListToDictionaryまたはToArrayを呼び出すと、列挙の即時評価が強制され、すべての要素がコレクションに取得されます。

レイジー評価の例:フィボナッチ数

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics; // also add reference to System.Numberics

namespace ConsoleApplication33
{
    class Program
    {
        private static IEnumerable<BigInteger> Fibonacci()
        {
            BigInteger prev = 0;
            BigInteger current = 1;
            while (true)
            {
                yield return current;
                var next = prev + current;
                prev = current;
                current = next;
            }
        }

        static void Main()
        {
            // print Fibonacci numbers from 10001 to 10010
            var numbers = Fibonacci().Skip(10000).Take(10).ToArray();
            Console.WriteLine(string.Join(Environment.NewLine, numbers));
        }
    }
}

どのようにそれがフードの下で動作する(私は、IL Disaamblerツールで結果の.exeファイルを逆コンパイルすることをお勧めします):

  1. C#コンパイラは、 IEnumerable<BigInteger>IEnumerator<BigInteger><Fibonacci>d__0 d__0)を実装するクラスを生成します。
  2. このクラスは状態マシンを実装します。 Stateは、メソッドの現在の位置とローカル変数の値から構成されます。
  3. 最も興味深いコードはbool IEnumerator.MoveNext()メソッドにあります。基本的に、 MoveNext()は何をします:
    • 現在の状態を復元します。 prevcurrentなどの変数は、クラス内のフィールドになります( <current>5__2<prev>5__1 5__1)。我々の方法では、2つのポジション( <>1__state 1つの<>1__state )があります。最初は中括弧を開き、2番目はyield returnです。
    • 次のyield returnyield break / } yield breakまでコードを実行します。
    • yield return値は結果値が保存されるので、 Currentプロパティはそれを返すことができます。 trueが返されます。この時点で、現在の状態は、次のMoveNext呼び出しのために再び保存されます。
    • yield break / }メソッドの場合は、反復が行われたfalse意味するfalseを返します。

また、10001番目の数字は468バイトであることにも注意してください。ステートマシンは、 current変数とprev変数のみをフィールドとして保存します。シーケンスのすべての数値を最初から10000番目まで保存したい場合、消費されるメモリサイズは4メガバイトを超えます。したがって、適切に使用されると、レイジー評価は、場合によってはメモリフットプリントを減らすことができます。

休憩と利休の違い

使用してyield breakするとは対照的にbreak 1が考えることほど明白ではないかもしれません。インターネット上には2つの使い方が互換性があり、その違いを実証していない悪い例がたくさんあります。

混乱する部分は、両方のキーワード(またはキーフレーズ)がループ内でのみ意味があるということです( foreachwhile ...)。

あるメソッドでyieldキーワードを使うと、メソッドを効果的にイテレータに変えることができます 。このようなメソッドの唯一の目的は、有限または無限のコレクションを繰り返し処理し、その要素を生成(出力)することです。目的が達成されると、メソッドの実行を続ける理由はありません。時には、メソッドの最後の閉じ括弧で自然に発生します} 。しかし、時には、メソッドを途中で終了したいとします。通常の(反復しない)メソッドでは、 returnキーワードを使用します。しかし、イテレータでreturnを使うことはできません。yield yield breakを使用する必要があります。つまり、イテレータのyield breakは、標準メソッドのreturnと同じです。一方、 breakステートメントは最も近いループを終了させるだけです。

いくつかの例を見てみましょう:

    /// <summary>
    /// Yields numbers from 0 to 9
    /// </summary>
    /// <returns>{0,1,2,3,4,5,6,7,8,9}</returns>
    public static IEnumerable<int> YieldBreak()
    {
        for (int i = 0; ; i++)
        {
            if (i < 10)
            {
                // Yields a number
                yield return i;
            }
            else
            {
                // Indicates that the iteration has ended, everything 
                // from this line on will be ignored
                yield break;
            }
        }
        yield return 10; // This will never get executed
    }
    /// <summary>
    /// Yields numbers from 0 to 10
    /// </summary>
    /// <returns>{0,1,2,3,4,5,6,7,8,9,10}</returns>
    public static IEnumerable<int> Break()
    {
        for (int i = 0; ; i++)
        {
            if (i < 10)
            {
                // Yields a number
                yield return i;
            }
            else
            {
                // Terminates just the loop
                break;
            }
        }
        // Execution continues
        yield return 10;
    }


Modified text is an extract of the original Stack Overflow Documentation
ライセンスを受けた CC BY-SA 3.0
所属していない Stack Overflow