web-dev-qa-db-ja.com

「利回り」の適切な使用

yield キーワードはC#のそれらの キーワード の1つであり、私を曖昧にし続けているので、私はそれを正しく使用しているとは自信を持っていません。

次の2つのコードのうち、どちらが望ましいのか、またその理由は何ですか。

バージョン1: yield returnを使う

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>();
    }
}
840
senfo

私はリストの次の項目(あるいは次の項目のグループ)を計算するときにyield-returnを使う傾向があります。

あなたのバージョン2を使用して、あなたは戻る前に完全なリストを持っていなければなりません。 yield-returnを使うことで、戻る前に次のアイテムを手に入れるだけでよいのです。

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

Yield-returnが望ましいもう1つのケースは、IEnumerableが無限集合を表す場合です。素数のリスト、または乱数の無限リストを考えてみましょう。完全なIEnumerableを一度に返すことはできないため、yield-returnを使用してリストを段階的に返すことができます。

あなたの特定の例では、あなたは製品の完全なリストを持っているので、私はバージョン2を使います。

762
abelenky

一時的なリストを作成するのはビデオ全体をダウンロードするのと同じですが、yieldを使うのはそのビデオをストリーミングするのと同じです。

590
anar khalilov

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を使うべきです。

65
Kache

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

26
Robert Rossney

これは古い質問ですが、yieldキーワードをクリエイティブに使用する方法の一例を示したいと思います。私は本当ににこのテクニックの恩恵を受けています。うまくいけば、これはこの質問につまずく人への助けになるでしょう。

注:yieldキーワードを、単にコレクションを構築するための別の方法であると見なさないでください。イールドパワーの大部分は、呼び出し元のコードが次の値を繰り返すまで、メソッドまたはプロパティ内で paused が実行されることです。これが私の例です:

Yieldキーワードを(Rob Eisenburgの Caliburn.Microコルーチン implementationと共に)使用すると、このように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は、HandleButtonClick()の呼び出しからIEnumeratorを取得し、それを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; }
}

このようなものを設定して、実行をステップスルーして何が起こっているのかを監視することが役に立つかもしれません。

これが誰かに役立つことを願っています!私は本当に歩留まりを使用することができるさまざまな方法を模索して楽しんでいます。

25

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

    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(trips))
        {
            // possible trip is actually calculated only at this point, because of yield
            if (IsTripGood(trip))
            {
                // match good trip
            }
        }
    }

Yieldの代わりにListを使うと、100万個のオブジェクトをメモリ(〜190mb)に割り当てる必要があり、この簡単な例では実行に〜1400msかかります。ただし、yieldを使用すると、これらすべての一時オブジェクトをメモリに保存する必要がなくなり、アルゴリズムの速度が大幅に向上します。この例では、メモリをまったく消費せずに実行するのに約400msかかります。

13

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

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

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

12
Jason Baker

収量には2つの大きな用途があります

一時コレクションを作成せずにカスタム反復を提供するのに役立ちます。 (全データの読み込みとループ)

それはステートフルな繰り返しをするのを助けます。 (ストリーミング)

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

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

11

これが Chris SellThe C#Programming Language の中のこれらのステートメントについて語っていることです。

私は、イールドリターン後のコードを実行できるという点で、イールドリターンがreturnと同じではないことを時々忘れています。たとえば、ここで最初に戻った後のコードは実行できません。

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

これとは対照的に、ここで最初の利回りが返された後のコードを実行することができます。

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!
}

このような場合は、利回りのように利回りが最終的ではないことを覚えておくと便利です。

10
Teoman shipahi

あなたの製品のLINQクラスが列挙/反復のために同様の歩留まりを使用すると仮定すると、最初のバージョンはそれが反復されるたびにただ一つの値をもたらすのでより効率的です。

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

8
Soviut

これは要点以外のちょっとしたことですが、質問にはベストプラクティスというタグが付けられているので、先に進み、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);

vs

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

注: /私は彼らの仕事をするのに時間がかかるかもしれないどんなメソッドのためにもこれをしないでしょう。

8

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

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ステートメントで使用されているように見えます)、それを直接返します。

7
petr k.

この場合は、バージョン2のコードを使用したはずです。あなたが利用可能な製品の完全なリストを持っていて、それがこのメソッド呼び出しの「消費者」によって期待されるものであるので、それは呼び出し側に完全な情報を送り返すことが必要とされるでしょう。

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

利回りリターンを使用できるいくつかの例は次のとおりです。

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

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

5

リストを直接返す利点:

  • もっとはっきりしている
  • リストは再利用可能です。 (イテレータはそうではありません) 実際にはそうではありません、ありがとうジョン

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

3
recursive

イールドリターンキーフレーズは、特定のコレクションのステートマシンを維持するために使用されます。 CLRは、イールドリターンキーフレーズが使用されていることを確認するたびに、そのコードに対してEnumeratorパターンを実装します。このタイプの実装は、そうでなければキーワードがない場合にしなければならないすべてのタイプの配管から開発者を支援します。

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

この記事のキーワード .

1
Vikram