「利回り」の適切な使用


903

降伏キーワードは、それらの一つであるキーワードを私に神秘を続けてC#で、私はそれを正しく使用していることを私は確信していたことがありません。

次の2つのコードのうち、どちらが推奨され、なぜですか?

バージョン1:利回り収益の使用

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        foreach (Product product in products)
        {
            yield return product;
        }
    }
}

バージョン2:リストを返す

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList<Product>();
    }
}

38
yieldIEnumerable<T>とその種類に関連付けられています。いずれにせよ怠惰な評価
Jaider

これは、同様の質問に対するすばらしい答えです。 stackoverflow.com/questions/15381708/...
サンジーブライ

1
ここでは良い使用方法の例を示します。stackoverflow.com/questions/3392612/...
ValGe

6
yield returnの結果を反復処理するコードGetAllProducts()によって、ユーザーが処理を途中でキャンセルする機会が得られる場合は、使用するのに適したケースが見られます。
JMD 2014年

2
私はこのスレッドが本当に役立つと感じました: programmers.stackexchange.com/a/97350/148944
PiotrWolkowski

回答:


806

リストの次のアイテム(またはアイテムの次のグループ)を計算するときは、yield-returnを使用する傾向があります。

バージョン2を使用する場合は、戻る前に完全なリストが必要です。yield-returnを使用することで、実際に戻る前に次のアイテムを取得するだけで済みます。

とりわけ、これは複雑な計算の計算コストをより大きな時間枠に分散させるのに役立ちます。たとえば、リストがGUIに接続されていて、ユーザーが最後のページに移動しない場合、リストの最終的なアイテムを計算することはありません。

yield-returnが望ましいもう1つのケースは、IEnumerableが無限セットを表す場合です。素数のリスト、または乱数の無限リストを検討してください。IEnumerable全体を一度に返すことはできないため、yield-returnを使用してリストを段階的に返します。

特定の例では、製品の完全なリストがあるので、バージョン2を使用します。


31
質問3の例では、2つの利点が強調されています。1)計算コストが分散します(場合によってはメリットであり、場合によってはメリットがありません)。あなたはそれが中間状態を維持している潜在的な欠点に言及することに失敗します。中間状態が大量にある場合(たとえば、重複排除のためのHashSet)、yieldを使用すると、メモリフットプリントが増大する可能性があります。
ケネットベレンキー

8
また、個々の要素が非常に大きいが、順番にアクセスするだけでよい場合は、歩留まりが向上します。
ケネットベレンキー

2
そして最後に...非常にシリアライズされた形式で非同期コードを記述するためにyieldを使用するための少しぎこちないが、時には効果的なテクニックがあります。
ケネットベレンキー

12
興味深いもう1つの例は、かなり大きなCSVファイルを読み取る場合です。各要素を読み取りたいが、依存関係を抽出したい場合もある。IEnumerable <>を返すYieldを使用すると、各行を返し、各行を個別に処理できます。10 Mbファイルをメモリに読み込む必要はありません。一度に1行だけ。
Maxime Rouiller 2013年

1
Yield return独自のカスタムイテレータクラス(IEnumeratorを実装)を記述するための省略形のようです。したがって、前述の利点はカスタムイテレータクラスにも適用されます。とにかく、両方の構成要素は中間状態を維持します。最も単純な形式では、現在のオブジェクトへの参照を保持します。
J. Ouwehand

641

一時的なリストへの入力はビデオ全体のダウンロードに似ていますが、使用yieldはそのビデオのストリーミングに似ています。


180
この回答が技術的な回答ではないことは完全に承知していますが、yieldとビデオストリーミングの類似性は、yieldキーワードを理解するための良い例となると思います。このテーマについては技術的なことはすべて述べられているので、「言い換えれば」説明しようと思いました。技術的でない用語で自分のアイデアを説明できないというコミュニティルールはありますか?
anar khalilov 2013

13
誰があなたに反対票を投じたのか、なぜそうしたのかはわかりませんが(コメントしてくれるといいのですが)、それは技術的ではない見込み客からの説明だと思います。
senfo 2013

22
まだコンセプトとこれを把握していることは、それをさらに焦点を合わせるのに役立ちました。
トニー

11
私はこの答えが好きですが、質問には答えません。
ANeves、2015年

73

を使用する必要がある場合を理解するための概念的な例として、yieldメソッドConsumeLoop()がによって返される/生成されるアイテムを処理するとしますProduceList()

void ConsumeLoop() {
    foreach (Consumable item in ProduceList())        // might have to wait here
        item.Consume();
}

IEnumerable<Consumable> ProduceList() {
    while (KeepProducing())
        yield return ProduceExpensiveConsumable();    // expensive
}

を使用しないyieldと、ProduceList()に戻る前にリストを完成させる必要があるため、への呼び出しに時間がかかる場合があります。

//pseudo-assembly
Produce consumable[0]                   // expensive operation, e.g. disk I/O
Produce consumable[1]                   // waiting...
Produce consumable[2]                   // waiting...
Produce consumable[3]                   // completed the consumable list
Consume consumable[0]                   // start consuming
Consume consumable[1]
Consume consumable[2]
Consume consumable[3]

を使用yieldすると、並べ替えられ、「並行して」動作するようになります。

//pseudo-assembly
Produce consumable[0]
Consume consumable[0]                   // immediately Consume
Produce consumable[1]
Consume consumable[1]                   // consume next
Produce consumable[2]
Consume consumable[2]                   // consume next
Produce consumable[3]
Consume consumable[3]                   // consume next

そして最後に、これまでに多くの人がすでに提案しているように、とにかく完全なリストをすでに持っているので、バージョン2を使用する必要があります。


30

これは古い質問であることは承知していますが、yieldキーワードをクリエイティブに使用する方法の一例を紹介します。私は本当に持っていますこのテクニックから恩恵を受けています。うまくいけば、これがこの質問に遭遇した他の誰にとっても助けになるでしょう。

注:yieldキーワードを、単にコレクションを構築する別の方法であると考えないでください。利回りの力の大部分は、実行が一時停止されるという事実にあります呼び出しコードが次の値を反復するまで、メソッドまたはプロパティでれるます。これが私の例です:

yieldキーワードを(Rob EisenburgのCaliburn.Microコルーチンの実装とともに)使用すると、次のようなWebサービスへの非同期呼び出しを表現できます。

public IEnumerable<IResult> HandleButtonClick() {
    yield return Show.Busy();

    var loginCall = new LoginResult(wsClient, Username, Password);
    yield return loginCall;
    this.IsLoggedIn = loginCall.Success;

    yield return Show.NotBusy();
}

これにより、BusyIndi​​catorがオンになり、WebサービスでLoginメソッドが呼び出され、IsLoggedInフラグが戻り値に設定され、BusyIndi​​catorがオフになります。

これがどのように機能するかを以下に示します。IResultには、ExecuteメソッドとCompletedイベントがあります。Caliburn.Microは、IEnumeratorをHandleButtonClick()の呼び出しから取得し、Coroutine.BeginExecuteメソッドに渡します。BeginExecuteメソッドは、IResultsを通じて反復を開始します。最初のIResultが返されると、実行がHandleButtonClick()内で一時停止され、BeginExecute()がイベントハンドラーをCompletedイベントにアタッチして、Execute()を呼び出します。IResult.Execute()は同期タスクまたは非同期タスクを実行でき、完了時にCompletedイベントを発生させます。

LoginResultは次のようになります。

public LoginResult : IResult {
    // Constructor to set private members...

    public void Execute(ActionExecutionContext context) {
        wsClient.LoginCompleted += (sender, e) => {
            this.Success = e.Result;
            Completed(this, new ResultCompletionEventArgs());
        };
        wsClient.Login(username, password);
    }

    public event EventHandler<ResultCompletionEventArgs> Completed = delegate { };
    public bool Success { get; private set; }
}

このようなものをセットアップし、実行をステップ実行して何が起こっているかを監視すると役立つ場合があります。

これが誰かを助けることを願っています!利回りを使用するさまざまな方法を探求することを本当に楽しんでいます。


1
コードサンプルは、forまたはforeachブロックのyield OUTSIDEの使用方法に関する優れた例です。ほとんどの例は、イテレータ内の利回りを示しています。SOについて質問しようとしたところ、非常に役に立ちました。イテレータの外でyieldを使用する方法です。
シェルペペレイラ

yieldこのように使用することは私には一度もありませんでした。これは、非同期/待機パターンをエミュレートするエレガントな方法のように見えます(yieldこれが今日書き換えられた場合の代わりに使用されると思います)。yieldあなたがこの質問に答えてからC#が進化してきたため、これらのクリエイティブな使用により、何年にもわたって収益が減少している(しゃれた意図はない)ことがわかりましたか?それとも、このような近代化された賢いユースケースをまだ考えていますか?もしそうなら、私たちのために別の興味深いシナリオを共有していただけませんか?
偏った

27

これは奇妙な提案のように思われますがyield、Pythonでのジェネレーターに関するプレゼンテーションを読んでC#でキーワードを使用する方法を学びました:David M. Beazleyのhttp://www.dabeaz.com/generators/Generators.pdf。プレゼンテーションを理解するのに、Pythonをよく知る必要はありません-私は知りませんでした。ジェネレーターがどのように機能するかだけでなく、なぜ気にかけるべきかを説明するのに非常に役立ちました。


1
プレゼンテーションは簡単な概要を提供します。C#での動作の詳細については、Ray Chenがstackoverflow.com/a/39507/939250 のリンクで説明しています。最初のリンクは、yield returnメソッドの最後に2番目の暗黙的な戻りがあることを詳しく説明しています。
Donal Lafferty、2012

18

イールドリターンは、何百万ものオブジェクトを反復処理する必要があるアルゴリズムにとって非常に強力です。配車のために可能なトリップを計算する必要がある次の例を検討してください。まず、可能な旅行を生成します。

    static IEnumerable<Trip> CreatePossibleTrips()
    {
        for (int i = 0; i < 1000000; i++)
        {
            yield return new Trip
            {
                Id = i.ToString(),
                Driver = new Driver { Id = i.ToString() }
            };
        }
    }

次に、各旅行を繰り返します。

    static void Main(string[] args)
    {
        foreach (var trip in CreatePossibleTrips())
        {
            // possible trip is actually calculated only at this point, because of yield
            if (IsTripGood(trip))
            {
                // match good trip
            }
        }
    }

収量の代わりにリストを使用する場合、100万個のオブジェクトをメモリ(約190 MB)に割り当てる必要があり、この単純な例の実行には約1400ミリ秒かかります。ただし、yieldを使用する場合は、これらのすべての一時オブジェクトをメモリに配置する必要はなく、アルゴリズムの速度が大幅に向上します。この例では、メモリを消費せずに実行するのに400msしかかかりません。


2
カバーの下で収量とは何ですか?リストだと思っていたので、メモリ使用量をどのように改善できますか?
2017年

1
@rolls yieldは、内部でステートマシンを実装することにより、内部で機能します。これは、実装を非常に詳細に説明する3つの詳細なMSDNブログ投稿含むSOの回答です。Raymond Chen @ MSFTによって作成
Shiva

13

2つのコードは、実際には2つの異なることを行っています。最初のバージョンでは、必要に応じてメンバーをプルします。2番目のバージョンでは、すべての結果をメモリロードしてから、何かを開始します。

これには正解も不正解もありません。どちらが好ましいかは、状況によって異なります。たとえば、クエリを完了する必要がある時間の制限があり、結果をある程度複雑にする必要がある場合は、2番目のバージョンが適しています。ただし、特にこのコードを32ビットモードで実行している場合は、大きな結果セットに注意してください。このメソッドを実行すると、OutOfMemory例外に何度か噛まれました。

ただし、重要な点はこれです。違いは効率にあります。したがって、おそらく、コードを単純化する方を使用し、プロファイリング後にのみ変更する必要があります。


11

利回りには2つの大きな用途があります

一時的なコレクションを作成せずにカスタムの反復を提供するのに役立ちます。(すべてのデータをロードしてループ)

ステートフルな反復を行うのに役立ちます。(ストリーミング)

以下は、上記の2つのポイントをサポートするために完全なデモンストレーションで作成した簡単なビデオです。

http://www.youtube.com/watch?v=4fju3xcm21M


10

これは、Chris SoldC#プログラミング言語これらのステートメントについて語っていることです。

ときどき、yield returnがreturnと同じではないことを忘れます。これは、yield returnの後のコードを実行できることです。たとえば、ここで最初に戻った後のコードは実行できません。

    int F() {
return 1;
return 2; // Can never be executed
}

対照的に、ここで最初のyield returnの後のコードは実行できます:

IEnumerable<int> F() {
yield return 1;
yield return 2; // Can be executed
}

これはしばしばifステートメントで私を噛みます:

IEnumerable<int> F() {
if(...) { yield return 1; } // I mean this to be the only
// thing returned
yield return 2; // Oops!
}

これらの場合、利回りのリターンはリターンのように「最終」ではないことを覚えておくと役に立ちます。


あいまいさを減らすために、できると言ったときに明確にしてください。最初のものが戻って2番目の利回りを実行しない可能性はありますか?
Johno Crawford、

@JohnoCrawford 2番目のyieldステートメントは、IEnumerableの2番目/次の値が列挙されている場合にのみ実行されます。たとえばF().Any()、最初の結果のみを列挙しようとした後に返されるなど、完全に不可能である可能性があります。一般的には、あなたがに頼るべきではありませんIEnumerable yield、それが実際にトリガーされないことがありますので、プログラムの状態を変更するには
ザックFaragher

8

製品のLINQクラスが列挙/反復に同様のイールドを使用すると仮定すると、最初のバージョンは反復するたびに1つの値しか生成しないため、より効率的です。

2番目の例は、ToList()メソッドを使用して列挙子/反復子をリストに変換することです。つまり、列挙子内のすべての項目を手動で反復処理してから、フラットリストを返します。


8

これはちょっとポイントの外ですが、質問にはベストプラクティスがタグ付けされているので、先に進んで2セントを投入します。このタイプのものについては、私はそれをプロパティにすることを非常に好みます:

public static IEnumerable<Product> AllProducts
{
    get {
        using (AdventureWorksEntities db = new AdventureWorksEntities()) {
            var products = from product in db.Product
                           select product;

            return products;
        }
    }
}

確かに、これはもう少し定型的ですが、これを使用するコードははるかにきれいに見えます。

prices = Whatever.AllProducts.Select (product => product.price);

prices = Whatever.GetAllProducts().Select (product => product.price);

注:処理に時間がかかる可能性があるメソッドについては、これを行いません。


7

そして、これはどうですか?

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList();
    }
}

これはもっときれいだと思います。VS2008はまだ手元にありません。いずれにせよ、ProductsがIEnumerableを実装している場合(見たところ、foreachステートメントで使用されているようです)、直接返します。


2
回答を投稿するのではなく、OPを編集して詳細情報を含めてください。
Brian Rasmussen、

まあ、あなたは私にOPが正確に何を
意味する

元の投稿だと思います。私は投稿を編集できないので、これは進むべき道のようです。
petr k。

5

この場合、コードのバージョン2を使用しました。利用可能な製品の完全なリストがあり、それがこのメソッド呼び出しの「コンシューマー」が期待するものであるため、完全な情報を呼び出し元に送り返す必要があります。

このメソッドの呼び出し元が一度に1つの情報を必要とし、次の情報の消費がオンデマンドベースである場合、yield returnを使用すると、実行コマンドが呼び出し元に確実に返されるようになります。情報の単位が利用可能です。

イールドリターンを使用できる例は次のとおりです。

  1. 呼び出し元が一度にステップのデータを待っている複雑な段階的な計算
  2. GUIのページング-ユーザーが最後のページに到達することはなく、情報のサブセットのみが現在のページに開示される必要がある場合

あなたの質問に答えるには、バージョン2を使用しました。


3

リストを直接返します。利点:

  • より明確です
  • リストは再利用可能です。(イテレータはそうではありません)実際にはそうではありません、ありがとうジョン

イテレータ(yield)は、リストの最後まで反復する必要がないと思われる場合、またはリストに最後がない場合に使用する必要があります。たとえば、クライアント呼び出しは、いくつかの述語を満たす最初の製品を検索することになります。イテレータの使用を検討することもできますが、これは不自然な例であり、おそらくそれを達成するためのより良い方法があるでしょう。基本的に、リスト全体を計算する必要があることが事前にわかっている場合は、前もって計算してください。そうでないと思われる場合は、イテレータバージョンの使用を検討してください。


IEnumerator <T>ではなく、IEnumerable <T>で返されることを忘れないでください。GetEnumeratorを再度呼び出すことができます。
ジョン・スキート

事前にリスト全体を計算する必要があることがわかっている場合でも、利回りのリターンを使用すると有益な場合があります。1つの例は、コレクションに数十万のアイテムが含まれている場合です。
Val

1

yield returnキーフレーズは、特定のコレクションの状態マシンを維持するために使用されます。CLRは、使用されているyield return keyphraseを検出すると、そのコードにEnumeratorパターンを実装します。このタイプの実装は、キーワードがない場合に行わなければならないすべてのタイプの配管から開発者を支援します。

開発者がいくつかのコレクションをフィルタリングし、コレクションを反復処理してから、いくつかの新しいコレクションでそれらのオブジェクトを抽出するとします。この種の配管は非常に単調です。

キーワードの詳細については、こちらの記事をご覧ください


-4

yieldの使い方は、ジェネレーターを返すことを除いて、キーワードreturnに似ています。そして、ジェネレーターオブジェクトは1回だけトラバースます。

利回りには2つの利点があります。

  1. これらの値を2回読み取る必要はありません。
  2. 多くの子ノードを取得できますが、それらをすべてメモリに配置する必要はありません。

多分あなたを助ける別の明確な説明があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.