web-dev-qa-db-ja.com

yieldを使用しない場合(リターン)

この質問にはすでに回答があります:
IEnumerableを返すときに「yield return」を使用しない理由はありますか?

SO yield return。例えば、

いつ[〜#〜] not [〜#〜]を使用するかについての考えを探していますyield return。たとえば、コレクション内のすべてのアイテムを返す必要がある場合、yieldのようなseemは役に立ちませんよね?

yieldの使用が制限、不必要、トラブルに巻き込まれる、または回避する必要がある場合はどうなりますか?

157

利回りの使用が制限されたり、不必要になったり、トラブルに巻き込まれたり、避けなければならないケースは何ですか?

再帰的に定義された構造を扱うときは、「利回りリターン」の使用について慎重に検討することをお勧めします。たとえば、私は頻繁にこれを見ます:

public static IEnumerable<T> PreorderTraversal<T>(Tree<T> root)
{
    if (root == null) yield break;
    yield return root.Value;
    foreach(T item in PreorderTraversal(root.Left))
        yield return item;
    foreach(T item in PreorderTraversal(root.Right))
        yield return item;
}

完全に賢明なコードですが、パフォーマンスの問題があります。ツリーの深さがhであるとします。その後、多くの場合、O(h)ネストされたイテレータが構築されます。外側のイテレータで「MoveNext」を呼び出すと、O(h)ネストされた呼び出しこれは、MoveNextに渡されます。これは、n個のアイテムを持つツリーに対して、これをO(n)回するため、アルゴリズムO(hn)になります。そして、バイナリツリーの高さはlg n <= h <= n。これは、アルゴリズムが最高でO(n lg n)、最低でO(n ^ 2)の時間、最高の場合O(lg n)および最悪の場合O(n)スタックスペース。O(h)ヒープスペースでは、各列挙子がヒープに割り当てられているためです。(C#の実装では、認識しています;準拠する実装他のスタックまたはヒープスペースの特性がある場合があります。)

しかし、ツリーを反復することは、O(n)時間内で、O(1)スタックスペース内である可能性があります。

public static IEnumerable<T> PreorderTraversal<T>(Tree<T> root)
{
    var stack = new Stack<Tree<T>>();
    stack.Push(root);
    while (stack.Count != 0)
    {
        var current = stack.Pop();
        if (current == null) continue;
        yield return current.Value;
        stack.Push(current.Left);
        stack.Push(current.Right);
    }
}

まだ利回りリターンを使用していますが、それについてはずっと賢いです。今、我々はO(n)時間であり、O(h)ヒープスペースであり、O(1)スタックスペース。

さらに読む:このテーマに関するWes Dyerの記事を参照してください。

http://blogs.msdn.com/b/wesdyer/archive/2007/03/23/all-about-iterators.aspx

142
Eric Lippert

利回りの使用が制限されたり、不必要になったり、トラブルに巻き込まれたり、避けなければならないケースは何ですか?

IEのいくつかのケースを考えることができます:

  • 既存のイテレータを返すときにyield returnを使用しないでください。例:

    // Don't do this, it creates overhead for no reason
    // (a new state machine needs to be generated)
    public IEnumerable<string> GetKeys() 
    {
        foreach(string key in _someDictionary.Keys)
            yield return key;
    }
    // DO this
    public IEnumerable<string> GetKeys() 
    {
        return _someDictionary.Keys;
    }
    
  • メソッドの実行コードを延期したくない場合は、yield returnを使用しないでください。例:

    // Don't do this, the exception won't get thrown until the iterator is
    // iterated, which can be very far away from this method invocation
    public IEnumerable<string> Foo(Bar baz) 
    {
        if (baz == null)
            throw new ArgumentNullException();
         yield ...
    }
    // DO this
    public IEnumerable<string> Foo(Bar baz) 
    {
        if (baz == null)
            throw new ArgumentNullException();
         return new BazIterator(baz);
    }
    
56
Pop Catalin

理解すべき重要なことは、yieldが何に役立つかということです。その場合、どのケースがそれから利益を得ないかを決定できます。

つまり、シーケンスを遅延評価する必要がない場合は、yieldの使用をスキップできます。それはいつですか?コレクション全体をすぐにメモリに入れてもかまわない場合です。それ以外の場合、メモリに悪影響を与える巨大なシーケンスがある場合は、yieldを使用して段階的に(つまり、遅延的に)作業する必要があります。プロファイラーは、両方のアプローチを比較する際に役立ちます。

ほとんどのLINQステートメントが_IEnumerable<T>_を返すことに注意してください。これにより、各ステップでパフォーマンスに悪影響を与えることなく、異なるLINQ操作を連続してつなぎ合わせることができます(別名、遅延実行)。代替画像は、各LINQステートメントの間にToList()呼び出しを配置することです。これにより、次の(チェーン)LINQステートメントを実行する前に、先行する各LINQステートメントがすぐに実行されるため、遅延評価の利点がなくなり、必要になるまで_IEnumerable<T>_を利用できます。

32
Ahmad Mageed

ここには多くの優れた答えがあります。私はこれを追加します:既に値を知っている小さなまたは空のコレクションにはyield returnを使用しないでください:

IEnumerable<UserRight> GetSuperUserRights() {
    if(SuperUsersAllowed) {
        yield return UserRight.Add;
        yield return UserRight.Edit;
        yield return UserRight.Remove;
    }
}

これらの場合、Enumeratorオブジェクトの作成は、単にデータ構造を生成するよりも高価で冗長です。

IEnumerable<UserRight> GetSuperUserRights() {
    return SuperUsersAllowed
           ? new[] {UserRight.Add, UserRight.Edit, UserRight.Remove}
           : Enumerable.Empty<UserRight>();
}

更新

私のベンチマーク の結果は次のとおりです。

Benchmark Results

これらの結果は、操作を1,000,000回実行するのにかかった時間(ミリ秒単位)を示しています。数値が小さいほど優れています。

これを再考すると、パフォーマンスの違いは心配するほど重要ではないので、読みやすく保守しやすいものを選択する必要があります。

更新2

上記の結果は、コンパイラーの最適化を無効にして達成されたと確信しています。最新のコンパイラを使用してリリースモードで実行すると、パフォーマンスは2つの間で実質的に区別できません。最も読みやすいものを選んでください。

23

Eric Lippertは良い点を挙げています(C#にはあまりにも悪い点がありません Cwのようなストリームのフラット化 )。列挙プロセスは他の理由で費用がかかる場合があるため、IEnumerableを複数回反復する場合はリストを使用する必要があることを付け加えます。

たとえば、LINQ-to-objectsは「yield return」に基づいて構築されます。遅いLINQクエリを作成した場合(たとえば、大きなリストを小さなリストにフィルター処理したり、並べ替えやグループ化を行う)場合、クエリの結果に対してToList()を順番に呼び出すのが賢明です複数回の列挙を回避するため(実際にクエリを複数回実行します)。

メソッドを作成するときに「yield return」とList<T>のいずれかを選択する場合は、次のことを考慮してください。各要素の計算に費用がかかり、呼び出し側は結果を複数回列挙する必要がありますか?答えがyesとyesであることがわかっている場合は、yield returnを使用しないでください(たとえば、生成されるリストが非常に大きく、使用するメモリを確保できない場合を除きます。yieldのもう1つのメリット結果リストが一度に完全にメモリにある必要はありません)。

「イールドリターン」を使用しないもう1つの理由は、インターリーブ操作が危険な場合です。たとえば、メソッドが次のように見える場合、

IEnumerable<T> GetMyStuff() {
    foreach (var x in MyCollection)
        if (...)
            yield return (...);
}

これは、呼び出し元が行うことによりMyCollectionが変更される可能性がある場合は危険です。

foreach(T x in GetMyStuff()) {
    if (...)
        MyCollection.Add(...);
        // Oops, now GetMyStuff() will throw an exception
        // because MyCollection was modified.
}

yield returnは、呼び出し側がyield関数が変更しないと仮定するものを変更するたびに問題を引き起こす可能性があります。

18
Qwertie

メソッドにメソッドの呼び出しで予想される副作用がある場合は、yield returnの使用を避けます。これは、 Pop Catalinが言及している 遅延実行によるものです。

副作用の1つは、システムを変更することです。これは、 単一責任原則 を破るIEnumerable<Foo> SetAllFoosToCompleteAndGetAllFoos()などのメソッドで発生する可能性があります。これはかなり明白ですが(今...)、それほど明白ではない副作用として、キャッシュされた結果を設定したり、最適化と同様のことがあります。

私の経験則(今も...)は次のとおりです。

  • 返されるオブジェクトが少しの処理を必要とする場合にのみyieldを使用します
  • yieldを使用する必要がある場合、メソッドに副作用はありません
  • 副作用が必要な場合(およびキャッシュなどに限定する場合)、yieldを使用しないでください。また、反復を拡張する利点がコストを上回ることを確認してください
6
Ben Scott

ランダムアクセスが必要な場合、歩留まりは制限/不必要になります。要素0にアクセスしてから要素99にアクセスする必要がある場合、遅延評価の有用性はほとんどなくなりました。

5
Robert Gowland

気付くかもしれないのは、列挙の結果をシリアル化し、それらをネットワーク経由で送信している場合です。結果が必要になるまで実行が延期されるため、空の列挙をシリアル化し、必要な結果の代わりにそれを送り返します。

5
Aidan

私は、yield returnとIEnumerableに完全に取りつかれている人からのコードの山を維持する必要があります。問題は、私たちが使用する多くのサードパーティAPIと多くの独自のコードが、リストまたは配列に依存していることです。だから私はやらなければならないことになります:

IEnumerable<foo> myFoos = getSomeFoos();
List<foo> fooList = new List<foo>(myFoos);
thirdPartyApi.DoStuffWithArray(fooList.ToArray());

必ずしも悪いわけではありませんが、対処するのは面倒ですし、場合によってはすべてをリファクタリングすることを避けるためにメモリ内に重複したリストを作成することになります。

2
Mike Ruhlin

基礎となるコレクションへの順次アクセスのためにコードブロックがイテレータを返さないようにする場合は、yield return。その後、単にコレクションをreturnします。

2
explorer

実際のLinqメンバーをラップするLinq-y拡張メソッドを定義している場合、それらのメンバーは反復子を返さない場合が多くあります。そのイテレータを自分で渡す必要はありません。

それを超えると、JITベースで評価される「ストリーミング」列挙型を定義するためにyieldを使用することで、実際に多くのトラブルに巻き込まれることはありません。

0
KeithS