web-dev-qa-db-ja.com

C#でyield returnイテレーターを使用する目的/利点は何ですか?

C#メソッド内でyield return x;を使用した例はすべて、リスト全体を返すだけで同じように実行できました。そのような場合、yield return構文を使用することと、リストを返すことのどちらに利点や利点がありますか?

また、完全なリストを返せなかったのはどのような種類のシナリオでyield returnを使用するでしょうか?

76
CoderDennis

しかし、自分でコレクションを作成している場合はどうでしょうか。

一般に、反復子はオブジェクトのシーケンスを遅延して生成するに使用できます。例えば ​​Enumerable.Rangeメソッドには、内部でいかなる種類のコレクションもありません。次の数値オンデマンドを生成するだけです。ステートマシンを使用したこの遅延シーケンス生成には多くの用途があります。それらのほとんどは関数型プログラミングの概念でカバーされています。

私の意見では、コレクションを列挙する方法と同じようにイテレータを見ている場合(これは最も単純な使用例の1つにすぎません)、間違った方向に進んでいることになります。言ったように、イテレータはシーケンスを返すための手段です。シーケンスはinfiniteの場合もあります。無限の長さのリストを返し、最初の100アイテムを使用する方法はありません。 has時々怠惰になること。 コレクションを返すことは、コレクションジェネレータを返すこととはかなり異なります(これはイテレータです)。リンゴとオレンジを比較しています。

仮説の例:

static IEnumerable<int> GetPrimeNumbers() {
   for (int num = 2; ; ++num) 
       if (IsPrime(num))
           yield return num;
}

static void Main() { 
   foreach (var i in GetPrimeNumbers()) 
       if (i < 10000)
           Console.WriteLine(i);
       else
           break;
}

この例では、10000未満の素数を出力します。素数生成アルゴリズムにまったく手を加えることなく、100万未満の数を出力するように簡単に変更できます。この例では、シーケンスは無限であり、消費者は最初から必要なアイテムの数すらわからないため、すべての素数のリストを返すことはできません。

115
Mehrdad Afshari

ここでの良い答えは、yield returnリストを作成する必要がないことです;リストは高価になる可能性があります。 (また、しばらくすると、それらはかさばってエレガントではなくなります。)

しかし、リストがない場合はどうなりますか?

yield returnを使用すると、さまざまな方法でデータ構造(必ずしもリストとは限りません)をトラバースできます。たとえば、オブジェクトがツリーの場合、他のリストを作成したり、基になるデータ構造を変更したりせずに、ノードを前後の順序でトラバースできます。

public IEnumerable<T> InOrder()
{
    foreach (T k in kids)
        foreach (T n in k.InOrder())
            yield return n;
    yield return (T) this;
}

public IEnumerable<T> PreOrder()
{
    yield return (T) this;
    foreach (T k in kids)
        foreach (T n in k.PreOrder())
            yield return n;
}
24
Ray

遅延評価/遅延実行

「yield return」イテレータブロックは、実際にその特定の結果を呼び出すまで、コードのanyを実行しません。つまり、効率的にチェーン化することもできます。ポップクイズ:次のコードはファイルを何回繰り返しますか?

var query = File.ReadLines(@"C:\MyFile.txt")
                            .Where(l => l.Contains("search text") )
                            .Select(l => int.Parse(l.SubString(5,8))
                            .Where(i => i > 10 );

int sum=0;
foreach (int value in query) 
{
    sum += value;
}

正解は1つで、foreachループのかなり下まで続きます。 3つの独立したlinq演算子関数がありますが、ファイルの内容をループするのは1回だけです。

これには、パフォーマンス以外の利点があります。たとえば、かなり単純なgenericメソッドを記述して、ログファイルを一度読み取り、事前フィルタリングし、同じ方法をいくつかの異なる場所で使用できます。 、それぞれの用途で異なるフィルターが追加されます。したがって、コードを効率的に再利用しながら、良好なパフォーマンスを維持します。

無限リスト

良い例については、この質問に対する私の回答を参照してください:
エラーを返すC#フィボナッチ関数

基本的に、停止しない(少なくともMaxIntに到達する前ではない)反復子ブロックを使用してフィボナッチシーケンスを実装し、その実装を安全な方法で使用します。

セマンティクスの改善と問題の分離

上記のファイルの例を再び使用して、ファイルを読み取るコードを、実際に結果を解析するコードから不要な行を除外するコードから簡単に分離できるようになりました。特に、最初のものは非常に再利用可能です。

これは、単純なビジュアルを持つ人よりも、散文で説明するのがはるかに難しいことの1つです。1

Imperative vs Functional Separation of Concerns

画像が表示されない場合は、同じコードの2つのバージョンが表示され、さまざまな懸念事項の背景が強調表示されています。 linqコードにはすべての色がうまくグループ化されていますが、従来の命令コードには色が混在しています。著者は、この結果はlinqを使用した場合と命令型コードを使用した場合の典型的なものであると主張しています(そして私は同意します)。


1 私はこれが元のソースであると信じています: https://Twitter.com/mariofusco/status/571999216039542784 。また、このコードはJavaですが、C#も同様です。

16
Joel Coehoorn

場合によっては、返す必要のあるシーケンスが大きすぎてメモリに収まらないことがあります。たとえば、約3か月前、私はMS SLQデータベース間のデータ移行プロジェクトに参加しました。データはXML形式でエクスポートされました。 Yield returnXmlReaderで非常に便利であることがわかりました。プログラミングが非常に簡単になりました。たとえば、ファイルに1000Customer要素があったとします。このファイルをメモリに読み込んだだけの場合、すべての要素をメモリに保存する必要があります。同時に処理される場合でも同じです。そのため、コレクションを1つずつ走査するためにイテレータを使用できます。その場合、1つの要素にメモリを費やす必要があります。

結局のところ、私たちのプロジェクトでXmlReaderを使用することがアプリケーションを機能させる唯一の方法でした-それは長い間機能しましたが、少なくともそれは機能しましたシステム全体をハングアップせず、OutOfMemoryExceptionを発生させませんでした。もちろん、yieldイテレータなしでXmlReaderを操作できます。しかし、イテレータは私の人生をはるかに楽にしてくれました(インポートのためのコードをそれほど速く、問題なく書くことはしませんでした)。これを見てください ページ 実際の問題を解決するために収量反復子がどのように使用されているかを確認します(無限シーケンスの科学的なものだけではありません)。

10
SPIRiT_1984

おもちゃ/デモのシナリオでは、大きな違いはありません。ただし、yieldイテレータが便利な場合があります-場合によっては、リスト全体(ストリームなど)が利用できない場合や、リストが計算コストが高く、リスト全体が必要になる可能性が低い場合があります。

9

これは、まったく同じ質問に対する私の以前に受け入れられた回答です。

収益キーワードの付加価値?

イテレータメソッドを確認する別の方法は、イテレータメソッドがアルゴリズムを「裏返し」にするというハードワークを行うことです。パーサーを考えてみましょう。ストリームからテキストを引き出し、パターンを探して、コンテンツの高レベルの論理的な説明を生成します。

これで、パターンの次の部分が見つかったときに通知するコールバックインターフェイスを使用するSAXアプローチを使用して、パーサー作成者としてこれを簡単に行うことができます。したがって、SAXの場合、要素の開始を見つけるたびに、beginElementメソッドなどを呼び出します。

しかし、これは私のユーザーに問題を引き起こします。ハンドラーインターフェイスを実装する必要があるため、コールバックメソッドに応答するステートマシンクラスを記述する必要があります。これを正しく行うのは難しいので、最も簡単な方法は、DOMツリーを構築するストック実装を使用することです。そうすれば、ツリーをたどることができるので便利です。しかし、その後、構造全体がメモリにバッファリングされます-良くありません。

しかし、代わりに、パーサーをイテレーターメソッドとして記述しますか?

IEnumerable<LanguageElement> Parse(Stream stream)
{
    // imperative code that pulls from the stream and occasionally 
    // does things like:

    yield return new BeginStatement("if");

    // and so on...
}

それは、コールバックインターフェイスアプローチよりも書くのは難しくありません。コールバックメソッドを呼び出す代わりに、LanguageElement基本クラスから派生したオブジェクトを返すだけです。

ユーザーはforeachを使用してパーサーの出力をループできるので、非常に便利な命令型プログラミングインターフェイスを利用できます。

その結果、カスタムAPIの両側は制御されているように見えますので、記述および理解が容易になります。

2

リスト全体が巨大な場合は、ただ座っているだけで多くのメモリを消費する可能性がありますが、利回りでは、アイテムの数に関係なく、必要なときに必要なものだけでプレイできます。

2
nilamo

怠惰な対積極的な評価 に関するEric Whiteのブログ(ちなみに優れたブログ)のこのディスカッションを見てください。

2
JP Alioto

yield returnを使用すると、リストを作成しなくてもアイテムを反復できます。リストは必要ないが、いくつかのアイテムのセットを反復処理したい場合は、簡単に書くことができます

foreach (var foo in GetSomeFoos()) {
    operate on foo
}

より

foreach (var foo in AllFoos) {
    if (some case where we do want to operate on foo) {
        operate on foo
    } else if (another case) {
        operate on foo
    }
}

Fooを操作するかどうかを決定するためのすべてのロジックを、yield returnを使用してメソッド内に置くことができ、foreachループはより簡潔になります。

2
AgileJon

Yieldを使用する基本的な理由は、それ自体でリストを生成/返すことです。返されたリストをさらに反復するために使用できます。

2