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


80

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

また、yield return完全なリストを返すことができなかったシナリオの種類は何ですか?


15
そもそもなぜ「リスト」があると思い込んでいるのですか?ない場合はどうなりますか?
Eric Lippert

2
@エリック、それが私が求めていたものだと思います。そもそもリストがないのはいつですか。これまでの回答では、ファイルストリームと無限シーケンスが2つの優れた例です。
CoderDennis 2009

1
あなたがリストを持っているなら、確かに、それを返すだけです。ただし、メソッド内でリストを作成してそれを返す場合は、代わりにイテレータを使用する必要があります。アイテムを1つずつ降ろします。多くの利点があります。
justin.m.chase 2009

2
5年前にこの質問をして以来、私は確かに多くのことを学びました!
CoderDennis 2014

1
yieldsを持つことの最大の利点は、さらに別の中間変数に名前を付ける必要がないことです。
nawfal 2016

回答:


120

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

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

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

架空の例:

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万未満の素数を出力するように簡単に変更できます。この例では、シーケンスが無限であり、消費者は最初から必要なアイテムの数さえ知らないため、すべての素数のリストを返すことはできません。


正しい。リストを作成しましたが、一度に1つのアイテムを返すのと、リスト全体を返すのとでは、どのような違いがありますか?
CoderDennis 2009

4
他の理由の中でも、コードがよりモジュール化されるため、アイテムをロードして処理し、繰り返すことができます。また、アイテムの読み込みに非常に費用がかかる場合や、アイテムがたくさんある場合を考えてみてください(数百万人が言う)。そのような場合、リスト全体をロードすることは望ましくありません。
Dana the Sane

15
@Dennis:メモリに線形に格納されたリストの場合、違いはないかもしれませんが、たとえば、10 GBのファイルを列挙し、各行を1つずつ処理する場合は、違いが生じます。
mmx 2009

1
優れた回答のための+ 1-また、yieldキーワードを使用すると、ネットワークソケット、Webサービス、さらには同時実行の問題など、従来はコレクションとは見なされていなかったソースにイテレーターセマンティクスを適用できます(stackoverflow.com/questions/を参照)。481714 / ccr-yield-and-vb-net
LBushkin 2009

良い例です。基本的には、コンテキスト(メソッド呼び出しなど)に基づいて、何かがアクセスを試みるまでアクションを開始しないコレクションジェネレーターですが、yieldのない従来のコレクションメソッドは、ビルドするサイズを知る必要があります。完全なコレクションを返します-次に、そのコレクションの必要な部分を繰り返しますか?
Michael Harper

24

ここでの細かい答え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;
}

1
この例では、委任の場合も強調しています。特定の状況下で他のコレクションのアイテムを含む可能性のあるコレクションがある場合、すべての結果の完全なリストを作成してそれを返す代わりに、反復してyieldreturnを使用するのは非常に簡単です。
トムメイフィールド

1
これで、C#yield!はF #と同じ方法で実装する必要があるため、すべてのforeachステートメントが必要になるわけではありません。
coderDennis 2012

ちなみに、あなたの例は次の「危険」の1つを示していますyield return:それが効率的または非効率的なコードをいつ生成するかはしばしば明らかではありません。がyield return再帰的に使用することができ、このような使用は、深くネストされた列挙子の処理に大きなオーバーヘッドを課します。手動の状態管理はコーディングがより複雑になる場合がありますが、はるかに効率的に実行されます。
スーパーキャット2012年

17

遅延評価/遅延実行

「yieldreturn」イテレータブロックは、実際にその特定の結果を呼び出すまで、コードを実行しません。これは、それらを効率的に連鎖させることもできることを意味します。ポップクイズ:次のコードはファイルを何回繰り返しますか?

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回だけです。

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

無限のリスト

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

基本的に、(少なくともMaxIntに到達する前に)停止しないイテレータブロックを使用してフィボナッチ数列を実装し、その実装を安全な方法で使用します。

セマンティクスの改善と関心の分離

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

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

関心の分離の命令型と機能型

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


1これが元のソースであると思います:https//twitter.com/mariofusco/status/571999216039542784。また、このコードはJavaですが、C#も同様であることに注意してください。


1
遅延実行は、おそらくイテレータの最大の利点です。
justin.m.chase 2009

12

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

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


9

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


2

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



2

を使用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を操作するかどうかを決定するためのすべてのロジックを配置でき、foreachループをはるかに簡潔にすることができます。


2

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

利回りキーワードの付加価値?

イテレータメソッドを調べる別の方法は、アルゴリズムを「裏返し」にするという大変な作業を行うことです。パーサーについて考えてみましょう。ストリームからテキストを取得し、その中のパターンを探して、コンテンツの高レベルの論理記述を生成します。

これで、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

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


概念的には正しいが、技術的には正しくない。単にイテレータを抽象化するIEnumerableのインスタンスを返します。そのイテレータは実際には次のアイテムを取得するためのロジックであり、具体化されたリストではありません。を使用return yieldすると、リストは生成されません。リスト内の次の項目のみが生成され、要求された場合(繰り返し)にのみ生成されます。
Sinaesthetic 2017年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.