収量:何、どこで、なぜ

開発者の.Netコミュニティは、C#7.0のリリースとそれがもたらす新機能を待っていました。 来年15歳になる言語の各バージョンには、何か新しい便利なものが付属していました。 すべての機能に個別の言及が必要ですが、今日はyield



キーワードについてお話したいと思います。 初心者の開発者(だけでなく)が使用を避けていることに気付きました。 この記事では、長所と短所を伝えるとともに、 yield



の使用yield



適切な場合を強調します。







yield



はイテレータを作成し、 IEnumerable



を実装するときに別のクラスを記述しないようにします。 C#には、 yield



を使用する2つの式が含まれますyield return <expression>



yield break



です。 yield



は、メソッド、演算子、およびプロパティで使用できます。 yield



はどこでも同じように機能するため、メソッドについて説明します。







yield return



を使用して、このメソッドが要素が各yield return



式の結果であるIEnumerable



シーケンスを返すことを宣言します。 さらに、戻り値を使用して、 yield return



は制御を呼び出し元に渡し、次の要素を要求した後、メソッドの実行を継続します。 yield



を含むメソッド内の変数の値は、リクエスト間で保存されます。 yield break



は、ループ内で使用される既知のbreak



役割を果たします。 以下の例は、0から10までの一連の数値を返します。







Getnumbers
 private static IEnumerable<int> GetNumbers() { var number = 0; while (true) { if (number > 10) yield break; yield return number++; } }
      
      





知っておく必要があるyield



使用にはいくつかの制限があることに言及することが重要です。 イテレータでReset



を呼び出すと、 NotSupportedException



スローされます。 匿名メソッドやunsafe



コードを含むメソッドでは使用できません。 また、 yield return



try-catch



に配置できませんが、 try-finally



ブロックのtry



セクションに配置されるのを止めるものはありません。 yield break



は、 try-catch



try-finally



両方のtry



セクションにあります。 Eric Lipertがここここで詳細に説明しているため、このような制限の理由は述べません。







コンパイル後にyield



が変わることを見てみましょう。 yield return



を使用する各メソッドは、イテレーター中に1つの状態から別の状態に移行する状態マシンです。 以下は、奇数の奇数列をコンソールに出力する単純なアプリケーションです。







プログラム例
 internal class Program { private static void Main() { foreach (var number in GetOddNumbers()) Console.WriteLine(number); } private static IEnumerable<int> GetOddNumbers() { var previous = 0; while (true) if (++previous%2 != 0) yield return previous; } }
      
      





コンパイラーは次のコードを生成します。







生成されたコード
 internal class Program { private static void Main() { IEnumerator<int> enumerator = null; try { enumerator = GetOddNumbers().GetEnumerator(); while (enumerator.MoveNext()) Console.WriteLine(enumerator.Current); } finally { if (enumerator != null) enumerator.Dispose(); } } [IteratorStateMachine(typeof(CompilerGeneratedYield))] private static IEnumerable<int> GetOddNumbers() { return new CompilerGeneratedYield(-2); } [CompilerGenerated] private sealed class CompilerGeneratedYield : IEnumerable<int>, IEnumerable, IEnumerator<int>, IDisposable, IEnumerator { private readonly int _initialThreadId; private int _current; private int _previous; private int _state; [DebuggerHidden] public CompilerGeneratedYield(int state) { _state = state; _initialThreadId = Environment.CurrentManagedThreadId; } [DebuggerHidden] IEnumerator<int> IEnumerable<int>.GetEnumerator() { CompilerGeneratedYield getOddNumbers; if ((_state == -2) && (_initialThreadId == Environment.CurrentManagedThreadId)) { _state = 0; getOddNumbers = this; } else { getOddNumbers = new CompilerGeneratedYield(0); } return getOddNumbers; } [DebuggerHidden] IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable<int>)this).GetEnumerator(); } int IEnumerator<int>.Current { [DebuggerHidden] get { return _current; } } object IEnumerator.Current { [DebuggerHidden] get { return _current; } } [DebuggerHidden] void IDisposable.Dispose() { } bool IEnumerator.MoveNext() { switch (_state) { case 0: _state = -1; _previous = 0; break; case 1: _state = -1; break; default: return false; } int num; do { num = _previous + 1; _previous = num; } while (num%2 == 0); _current = _previous; _state = 1; return true; } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } }
      
      





この例からわかるように、 yield



メソッドの本体は生成されたクラスに置き換えられyield



います。 メソッドのローカル変数がクラスフィールドに変わりました。 クラス自体はIEnumerable



IEnumerator



両方を実装します。 MoveNext



メソッドには、置き換えられたメソッドのロジックが含まれていますが、唯一の違いは、ステートマシンとして表されていることです。 元のメソッドの実装に応じて、生成されたクラスにはDispose



メソッドの実装が追加で含まれる場合があります。







2つのテストを実行して、パフォーマンスとメモリ消費を測定してみましょう。 これらのテストは総合的なものであり、「額」の実装と比較したyield



の操作を示すためにのみ提供されていることをすぐに言わなければなりません。 付属の診断モジュールBenchmarkDotNet.Diagnostics.Windows



BenchmarkDotNetを使用して測定を行います。 数値のシーケンスを取得する方法の速度を比較する最初の(アナログEnumerable.Range(start, count)



)。 最初のケースではイテレータなしの実装があり、2番目のケースでは以下があります。







テスト1
 public int[] Array(int start, int count) { var numbers = new int[count]; for (var i = 0; i < count; ++i) numbers[i] = start + i; return numbers; } public int[] Iterator(int start, int count) { return IteratorInternal(start, count).ToArray(); } private IEnumerable<int> IteratorInternal(int start, int count) { for (var i = 0; i < count; ++i) yield return start + i; }
      
      





方法 カウント 開始する 中央値 Stddev Gen 0 Gen 1 Gen 2 割り当てられたバイト数/ Op
配列 100 10 91.19 ns 1.25 ns 385.01 - - 169.18
イテレータ 100 10 1,173.26 ns 10.94 ns 1,593.00 - - 700.37


結果からわかるように、配列の実装は1桁高速であり、4分の1のメモリしか消費しません。 イテレーターと別個のToArray



呼び出しがトリックを行いました。







2番目のテストはより困難になります。 データストリームを使用して作業をエミュレートします。 最初に奇数のキーを持つエントリを選択し、次に3つのキーの倍数を持つエントリを選択します。 前のテストと同様に、最初の実装にはイテレータがなく、2番目の実装には次のものがあります。







テスト2
 public List<Tuple<int, string>> List(int start, int count) { var odds = new List<Tuple<int, string>>(); foreach (var record in OddsArray(ReadFromDb(start, count))) if (record.Item1%3 == 0) odds.Add(record); return odds; } public List<Tuple<int, string>> Iterator(int start, int count) { return IteratorInternal(start, count).ToList(); } private IEnumerable<Tuple<int, string>> IteratorInternal(int start, int count) { foreach (var record in OddsIterator(ReadFromDb(start, count))) if (record.Item1%3 == 0) yield return record; } private IEnumerable<Tuple<int, string>> OddsIterator(IEnumerable<Tuple<int, string>> records) { foreach (var record in records) if (record.Item1%2 != 0) yield return record; } private List<Tuple<int, string>> OddsArray(IEnumerable<Tuple<int, string>> records) { var odds = new List<Tuple<int, string>>(); foreach (var record in records) if (record.Item1%2 != 0) odds.Add(record); return odds; } private IEnumerable<Tuple<int, string>> ReadFromDb(int start, int count) { for (var i = start; i < count; ++i) yield return new KeyValuePair<int, string>(start + i, RandomString()); } private static string RandomString() { return Guid.NewGuid().ToString("n"); }
      
      





方法 カウント 開始する 中央値 Stddev Gen 0 Gen 1 Gen 2 割り当てられたバイト数/ Op
一覧 100 10 43.14 us 0.14 us 279.04 - - 4,444.14
イテレータ 100 10 43.22 us 0.76 us 231.00 - - 3,760.96


この場合、実行速度は同じであることが判明し、メモリ消費yield



はさらに低くなりました。 これは、イテレータを使用した実装では、コレクションが一度だけ計算され、1つのList<Tuple<int, string>>



割り当て時にメモリを節約したためです。







上記および上記のすべてのテストを考慮すると、簡単な結論を出すことができます。 yield



の主な欠点は、追加のイテレータクラスです。 シーケンスが有限であり、呼び出し元が要素を使用して複雑な操作を実行しない場合、反復子は遅くなり、GCに望ましくない負荷がかかります。 コレクションの各計算がメモリの大きな配列の割り当てにつながる場合、長いシーケンスを処理する場合にyield



を適用することが適切です。 yield



のレイジーな性質により、フィルター処理可能なシーケンス要素の計算が回避されます。 これにより、メモリ消費が大幅に削減され、プロセッサの負荷が軽減されます。








All Articles