C# Language
収穫のキーワード
サーチ…
前書き
ステートメントでyieldキーワードを使用する場合は、それが現れるメソッド、演算子、またはアクセサーをイテレータとして指定します。 yieldを使用してイテレータを定義すると、カスタムコレクション型のIEnumerableおよびIEnumeratorパターンを実装するときに、明示的な余分なクラス(列挙の状態を保持するクラス)が不要になります。
構文
- yield return [TYPE]
- 収穫逓減
備考
戻り値の型がIEnumerable
、 IEnumerable<T>
、 IEnumerator
、またはIEnumerator<T>
メソッドにyield
キーワードを設定すると、戻り値の型( IEnumerable
またはIEnumerator
)の実装が生成されます。それぞれの結果を得るためにそれぞれの "収量"までの
yield
キーワードは、論理的に無制限のシーケンスの「次の」要素を返す場合に便利です。事前にシーケンス全体を計算することは不可能です。また、返される前に完全なシーケンスを計算すると、ユーザーにとって望ましくない一時停止につながります。
yield break
を使用して、いつでも配列を終了することができます。
yield
キーワードはIEnumerable<T>
ような戻り型としてイテレータのインターフェイス型を必要とするため、これをTask<IEnumerable<T>>
オブジェクトを返すので非同期メソッドでは使用できません。
参考文献
簡単な使用法
yield
キーワードは、 IEnumerable
またはIEnumerator
( IEnumerator
派生した汎用バリアント)を返す関数を定義するために使用されます。この値は、呼び出し元が返されたコレクションを反復処理するときに遅延的に生成されます。 備考欄に目的の詳細をお読みください。
次の例は、 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
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 break
にyield 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
引数を正しくチェックする
イテレータメソッドは、戻り値が列挙されるまで実行されません。したがって、イテレータの外部で前提条件を宣言することは有益です。
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
メソッドが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();
ToList
、 ToDictionary
または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ファイルを逆コンパイルすることをお勧めします):
- C#コンパイラは、
IEnumerable<BigInteger>
とIEnumerator<BigInteger>
(<Fibonacci>d__0
d__0)を実装するクラスを生成します。 - このクラスは状態マシンを実装します。 Stateは、メソッドの現在の位置とローカル変数の値から構成されます。
- 最も興味深いコードは
bool IEnumerator.MoveNext()
メソッドにあります。基本的に、MoveNext()
は何をします:- 現在の状態を復元します。
prev
やcurrent
などの変数は、クラス内のフィールドになります(<current>5__2
と<prev>5__1
5__1)。我々の方法では、2つのポジション(<>1__state
1つの<>1__state
)があります。最初は中括弧を開き、2番目はyield return
です。 - 次の
yield return
かyield break
/}
yield break
までコードを実行します。 -
yield return
値は結果値が保存されるので、Current
プロパティはそれを返すことができます。true
が返されます。この時点で、現在の状態は、次のMoveNext
呼び出しのために再び保存されます。 -
yield break
/}
メソッドの場合は、反復が行われたfalse
意味するfalse
を返します。
- 現在の状態を復元します。
また、10001番目の数字は468バイトであることにも注意してください。ステートマシンは、 current
変数とprev
変数のみをフィールドとして保存します。シーケンスのすべての数値を最初から10000番目まで保存したい場合、消費されるメモリサイズは4メガバイトを超えます。したがって、適切に使用されると、レイジー評価は、場合によってはメモリフットプリントを減らすことができます。
休憩と利休の違い
使用してyield break
するとは対照的にbreak
1が考えることほど明白ではないかもしれません。インターネット上には2つの使い方が互換性があり、その違いを実証していない悪い例がたくさんあります。
混乱する部分は、両方のキーワード(またはキーフレーズ)がループ内でのみ意味があるということです( foreach
、 while
...)。
あるメソッドで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;
}