C#での「yield」キーワードの実際的な使用[非公開]


76

ほぼ4年間の経験の後、yieldキーワードが使用されているコードを見たことはありません。誰かがこのキーワードの(説明に沿って)実用的な使用法を私に示すことができますか?


9
LINQのすべて(または少なくともほとんど)はyieldを使用して実装されます。また、Unity3Dフレームワークにはいくつかの適切な使用法があります-関数を(yieldステートメントで)一時停止し、IEnumerableの状態を使用して後で再開するために使用されます。
ダニ

2
これをStackOverflowに移動してはいけませんか?
ダニーヴァロッド

4
@Danny-質問は特定の問題を解決するのではなくyield、一般的に何に使用できるのかを尋ねるので、Stack Overflowには適していません。
ChrisF

9
実際に?使用していない単一のアプリを考えることはできません。
アーロンノート

回答:


107

効率

このyieldキーワードは、コレクションアイテムの遅延列挙を効率的に作成し、はるかに効率的にすることができます。たとえば、foreachループが100万アイテムの最初の5アイテムのみを反復する場合、それはすべてyield戻り、最初に内部で100万アイテムのコレクションを構築しませんでした。同様に、独自のプログラミングシナリオyieldIEnumerable<T>戻り値を使用して、同じ効率を達成することができます。

特定のシナリオで得られた効率の例

イテレータメソッドではなく、大きなコレクションを非効率的に使用する可能性があります
(中間コレクションは多くのアイテムを使用して構築されます)

// Method returns all million items before anything can loop over them. 
List<object> GetAllItems() {
    List<object> millionCustomers;
    database.LoadMillionCustomerRecords(millionCustomers); 
    return millionCustomers;
}

// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in GetAllItems())  {
    num++;
    if (num == 5)
        break;
}
// Note: One million items returned, but only 5 used. 

イテレータバージョン、効率的
(中間コレクションは作成されません)

// Yields items one at a time as the caller's foreach loop requests them
IEnumerable<object> IterateOverItems() {
    for (int i; i < database.Customers.Count(); ++i)
        yield return database.Customers[i];
}

// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in IterateOverItems())  {
    num++;
    if (num == 5)
        break;
}
// Note: Only 5 items were yielded and used out of the million.

いくつかのプログラミングシナリオを簡素化する

別のケースでは、yieldアイテムを中間コレクションに並べ替えてスワップするのではなく、目的の順序に戻すだけなので、リストの並べ替えとマージを簡単に行うことができます。このようなシナリオは数多くあります。

1つの例は、2つのリストのマージです。

IEnumerable<object> EfficientMerge(List<object> list1, List<object> list2) {
    foreach(var o in list1) 
        yield return o; 
    foreach(var o in list2) 
        yield return o;
}

この方法では、アイテムの1つの連続したリストが返され、中間コレクションを必要とせずに効果的にマージされます。

詳細情報

yieldキーワードのみ(の戻り型を有するイテレータ方法の文脈で使用することができIEnumerableIEnumeratorIEnumerable<T>、またはIEnumerator<T>。)で特別な関係がありますforeach。イテレータは特別なメソッドです。MSDN降伏文書イテレータドキュメントは興味深い情報や概念の説明の多くが含まれています。それを関連付けるようにしてくださいキーワードのイテレータのご理解を補うために、あまりにもそれについて読んで。foreach

反復子がどのように効率を達成するかを知るために、秘密はC#コンパイラーによって生成されたILコードにあります。イテレータメソッド用に生成されるILは、通常の(非イテレータ)メソッド用に生成されるILとは大幅に異なります。この記事(Yieldキーワードが実際に生成するものは、そのような洞察を提供します。


2
これらは、(おそらく長い)シーケンスを取り、マッピングが1対1でない別のシーケンスを生成するアルゴリズムに特に役立ちます。この例は、ポリゴンクリッピングです。特定のエッジは、一度クリップされると、多くのエッジを生成する場合も、まったくエッジを生成しない場合もあります。イテレータを使用すると、これを非常に簡単に表現できます。イテレータは、イテレータを記述する最適な方法の1つです。
ドナルドフェローズ

+1私が書き留めたよりもはるかに良い答え。また、パフォーマンスを向上させるには収量が良いことも学びました。
Jan_V

3
むかし、私はyieldを使用して、バイナリネットワークプロトコルのパケットを作成しました。C#で最も自然な選択のように見えました。
ジェルジAndrasek

4
database.Customers.Count()顧客の列挙全体を列挙しないため、すべてのアイテムを通過するためにより効率的なコードが必要ですか?
スティーブン

5
私をアナルと呼んでください、しかしそれは結合ではなく、結合です。(そしてlinqには既にConcatメソッドがあります。)
OldFart

4

少し前に私は実用的な例を持っていました、あなたがこのような状況を持っていると仮定しましょう:

List<Button> buttons = new List<Button>();
void AddButtons()
{
   for ( int i = 0; i <= 10; i++ ) {
      var button = new Button();
      buttons.Add(button);
      button.Click += (sender, e) => 
          MessageBox.Show(String.Format("You clicked button number {0}", ???));
   }
}

ボタンオブジェクトは、コレクション内での自分の位置を知りません。同じ制限がDictionary<T>他のコレクションタイプにも適用されます。

yieldキーワードを使用した私のソリューションは次のとおりです。

interface IHasId { int Id { get; set; } }

class IndexerList<T>: List<T>, IEnumerable<T> where T: IHasId
{
   List<T> elements = new List<T>();
   new public void Clear() { elements.Clear(); }
   new public void Add(T element) { elements.Add(element); }
   new public int Count { get { return elements.Count; } }    
   new public IEnumerator<T> GetEnumerator()
   {
      foreach ( T c in elements )
         yield return c;
   }

   new public T this[int index]
   {
      get
      {
         foreach ( T c in elements ) {
            if ( (int)c.Id == index )
               return c;
         }
         return default(T);
      }
   }
}

そして、それが私がそれを使用する方法です:

class ButtonWithId: Button, IHasId
{
   public int Id { get; private set; }
   public ButtonWithId(int id) { this.Id = id; }
}

IndexerList<ButtonWithId> buttons = new IndexerList<ButtonWithId>();
void AddButtons()
{
   for ( int i = 10; i <= 20; i++ ) {
      var button = new ButtonWithId(i);
      buttons.Add(button);
      button.Click += (sender, e) => 
         MessageBox.Show(String.Format("You clicked button number {0}", ( (ButtonWithId)sender ).Id));
   }
}

forインデックスを見つけるために、コレクションをループする必要はありません。私のボタンはIDを持っており、これはまたのインデックスとして使用されているIndexerList<T>ので、あなたは、IDのか、インデックス任意の冗長を避ける -それは私が好きなものです!インデックス/ IDは任意の番号にすることができます。


2

実際の例はここにあります:

http://www.ytechie.com/2009/02/using-c-yield-for-readability-and-performance.html

標準コードよりもyieldを使用することには多くの利点があります。

  • イテレータを使用してリストを作成する場合、戻り値を返すことができ、呼び出し元はその結果をリストにするかどうかを決定できます。
  • 呼び出し元は、反復で実行している範囲外の理由で反復をキャンセルすることもできます。
  • コードは少し短くなっています。

ただし、Jan_Vが言ったように(数秒で私をbeatりました:-)、内部でコンパイラは両方のケースでほぼ同一のコードを生成するため、それなしで生きることができます。


1

以下に例を示します。

https://bitbucket.org/ant512/workingweek/src/a745d02ba16f/source/WorkingWeek/Week.cs#cl-158

このクラスは、稼働週に基づいて日付計算を実行します。私はクラスのインスタンスに、ボブが平日の毎日9:30から17:30に働いており、昼休みは12:30に働いていることを伝えることができます。この知識により、AscendingShifts()関数は、指定された日付間で作業シフトオブジェクトを生成します。今年の1月1日から2月1日までのボブの勤務シフトをすべてリストするには、次のように使用します。

foreach (var shift in week.AscendingShifts(new DateTime(2011, 1, 1), new DateTime(2011, 2, 1)) {
    Console.WriteLine(shift);
}

このクラスは実際にはコレクションを反復処理しません。ただし、2つの日付間のシフトはコレクションと考えることができます。yieldオペレータは、コレクション自体を作成せずに、この想像コレクションを反復処理することが可能となります。


1

commandSQLコマンドテキスト、コマンドタイプを設定し、「コマンドパラメーター」のIEnumerableを返すクラスを持つ小さなdbデータレイヤーがあります。

基本的には、SqlCommand常にプロパティとパラメーターを手動で入力するのではなく、CLRコマンドを入力するという考え方です。

そのため、次のような関数があります。

IEnumerable<DbParameter> GetParameters()
{
    // here i do something like

    yield return new DbParameter { name = "@Age", value = this.Age };

    yield return new DbParameter { name = "@Name", value = this.Name };
}

このクラスを継承するcommandクラスには、プロパティAgeとがありNameます。

次にcommand、プロパティを埋めたオブジェクトdbを更新し、実際にコマンド呼び出しを行うインターフェイスに渡すことができます。

全体として、SQLコマンドの操作と入力の維持が非常に簡単になります。


1

マージのケースはすでに受け入れられた回答でカバーされていますが、yield-merge params extension method™を示します。

public static IEnumerable<T> AppendParams<T>(this IEnumerable<T> a, params T[] b)
{
    foreach (var el in a) yield return el;
    foreach (var el in b) yield return el;
}

これを使用して、ネットワークプロトコルのパケットを作成します。

static byte[] MakeCommandPacket(string cmd)
{
    return
        header
        .AppendParams<byte>(0, 0, 1, 0, 0, 1, 0x92, 0, 0, 0, 0)
        .AppendAscii(cmd)
        .MarkLength()
        .MarkChecksum()
        .ToArray();
}

MarkChecksumこの方法は、例えば、次のようになります。そしてyield、もあります:

public static IEnumerable<byte> MarkChecksum(this IEnumerable<byte> data, int pos = 6)
{
    foreach (byte b in data)
    {
        yield return pos-- == 0 ? (byte)data.Sum(z => z) : b;
    }
}

ただし、列挙メソッドでSum()などの集計メソッドを使用する場合は、個別の列挙プロセスをトリガーするため、注意が必要です。


1

Elastic Search .NETのサンプルリポジトリにはyield return、指定されたサイズのコレクションを複数のコレクションに分割するための優れた使用例があります。

https://github.com/elastic/elasticsearch-net-example/blob/master/src/NuSearch.Domain/Extensions/PartitionExtension.cs

public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int size)
    {
        T[] array = null;
        int count = 0;
        foreach (T item in source)
        {
            if (array == null)
            {
                array = new T[size];
            }
            array[count] = item;
            count++;
            if (count == size)
            {
                yield return new ReadOnlyCollection<T>(array);
                array = null;
                count = 0;
            }
        }
        if (array != null)
        {
            Array.Resize(ref array, count);
            yield return new ReadOnlyCollection<T>(array);
        }
    }

0

Jan_Vの答えを拡張して、それに関連する実世界のケースを見つけました。

FindFirstFile / FindNextFileのKernel32バージョンを使用する必要がありました。最初の呼び出しからハンドルを取得し、それを後続のすべての呼び出しに渡します。これを列挙子でラップすると、foreachで直接使用できるものが得られます。

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